perf!: yoke zero-copy node decoding and Jid Server enum - #513
Conversation
Replace `Jid { server: Cow<'static, str> }` and
`JidRef { server: Cow<'a, str> }` with a `Server` u8 enum mapping
the 11 known WhatsApp server constants. This shrinks Jid from 56
to 32 bytes and NodeValue from 56 to 40 bytes.
- Server enum: Pn, Lid, Group, Broadcast, Newsletter, Hosted,
HostedLid, Messenger, Interop, Bot, Legacy
- Server comparisons are now u8 == u8 instead of string comparison
- Delete cow_server_from_str() — no longer needed
- JidRef.server no longer borrows from the decode buffer, removing
a lifetime dependency that simplifies the upcoming yoke integration
- Unknown servers are now rejected at parse time (JidError)
BREAKING CHANGE: `Jid.server` is now `Server` enum instead of
`Cow<'static, str>`. `Jid::new()` takes `Server` instead of `&str`.
Use `Server::Pn`, `Server::Lid`, etc. for known servers.
`server.as_str()` returns the string representation.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMigrates node handling to zero-copy views and a typed JID Server: adds yoke-backed Changes
Sequence Diagram(s)sequenceDiagram
participant Net as Network
participant Cli as Client
participant Dec as Decoder
participant Own as OwnedNodeRef
participant Rout as Router
participant Hnd as Handler
participant Wait as Waiters
Net->>Cli: receive protocol bytes
Cli->>Dec: decode bytes -> NodeRef view
Dec->>Own: OwnedNodeRef::new(buffer) (yoke)
Cli->>Rout: dispatch(Arc<OwnedNodeRef>)
Rout->>Hnd: handler.handle(Arc<OwnedNodeRef>)
Hnd->>Cli: call client.* using node.get() -> &NodeRef
Hnd->>Wait: resolve/fulfil waiter with Arc<OwnedNodeRef>
Cli->>Net: send outgoing stanza / ACK
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
@claude please review carefully, and also spot inconsistencies, more performance improvement (reducing unnecessary clones, allocations, conversions, etc.), and also try to find regressions, bugs, etc. |
|
Claude finished @jlucaso1's task in 6m 0s —— View job PR Review: Perf/yoke zero copy and server enum
This PR has two excellent changes bundled together: (1) replacing Part 1:
|
| Area | Status |
|---|---|
Server enum (jid.rs, encoder, decoder) |
Complete, clean |
| IQ spec migration (14 files) | Complete, clean |
| send.rs / message.rs / pair_code.rs | Complete, clean |
OwnedNodeRef + yoke design |
Complete, well-designed |
decrypt_frame → OwnedNodeRef |
Complete, correct |
process_node body migration |
Partially done, ~6 remaining issues |
| Handler chain (8 handlers) | Signatures updated, bodies not migrated |
| Router tests | Not yet updated |
| Downstream APIs (unified_session, build_ack_node, etc.) | Not yet updated |
The Server enum change is ready to ship. The yoke integration design is solid but needs the handler bodies and downstream API signatures completed. I'd estimate ~16 compilation fixes remaining to get this building. The "fix forward" approach (updating one handler at a time, verifying with cargo check) would be the most efficient path.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f45702548e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let server = | ||
| crate::jid::Server::try_from(server_str.as_ref()).unwrap_or(crate::jid::Server::Pn); |
There was a problem hiding this comment.
Preserve unknown JID domains when decoding JID_PAIR
Defaulting unknown JID_PAIR servers to Server::Pn silently rewrites identities from unsupported/new domains into @s.whatsapp.net, which can route stanzas down the wrong PN-specific code paths (session lookup/deletion, recipient resolution, and user identity matching) instead of failing fast or preserving the original domain. This regression is triggered whenever the wire payload contains a server string outside the current enum (including future WhatsApp domains).
Useful? React with 👍 / 👎.
| fn is_empty(&self) -> bool { | ||
| self.server().is_empty() | ||
| self.user().is_empty() |
There was a problem hiding this comment.
Keep server-only JIDs non-empty
Changing is_empty() to check only user makes valid server-only JIDs (for example "s.whatsapp.net") appear empty. RequestUtils::build_iq_node gates target emission on !target.is_empty(), so any IQ that intentionally uses a server-only target will now silently drop the target attribute and send a different request than intended.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
wacore/binary/src/jid.rs (2)
705-710:⚠️ Potential issue | 🟠 MajorSuppress
.agentforServer::HostedLidtoo.Both
Displayimpls still append.agentfor@hosted.lidwhenagent > 0. That changes the canonical string form touser.<agent>:<device>@hosted.lid, which is inconsistent with the@hostedbranch and can break round-trips for decoded hosted-LID JIDs.Suggested fix
- if !matches!(self.server, Server::Pn | Server::Lid | Server::Hosted) { + if !matches!( + self.server, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid + ) { write!(f, ".{}", self.agent)?; } } @@ - if self.agent > 0 && !matches!(self.server, Server::Pn | Server::Lid | Server::Hosted) { + if self.agent > 0 + && !matches!( + self.server, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid + ) + { write!(f, ".{}", self.agent)?; }Also applies to: 723-731
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 705 - 710, The Display implementations for JID append ".{agent}" when self.agent > 0 but currently skip only Server::Pn, Server::Lid, and Server::Hosted; update the condition to also skip Server::HostedLid so the agent is not appended for hosted-lid addresses (preventing outputs like user.<agent>:<device>@hosted.lid); locate the branches that check matches!(self.server, Server::Pn | Server::Lid | Server::Hosted) in the Display impl(s) (the blocks that call write!(f, ".{}", self.agent)?), add Server::HostedLid to that matches! list in both places (the other occurrence around the 723-731 range) so hosted-lid follows the same suppression logic.
652-685:⚠️ Potential issue | 🟠 MajorReject oversized agent values before casting to
u8.In the
user.agent:device@serverfallback path,num_val as u8truncates silently.user.300:1@hostedcurrently parses as agent44instead of failing, while the sibling dotted-agent branch below correctly rejects out-of-range values.Suggested fix
if server != DEFAULT_USER_SERVER && server != HIDDEN_USER_SERVER && let Some((u, last_part)) = user.rsplit_once('.') && let Ok(num_val) = last_part.parse::<u16>() { user = u; - agent = num_val as u8; + if num_val > u8::MAX as u16 { + return Err(JidError::InvalidFormat(format!( + "Agent component out of range: {num_val}" + ))); + } + agent = num_val as u8; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 652 - 685, The early branch that handles dotted-agent when server != DEFAULT_USER_SERVER && server != HIDDEN_USER_SERVER (the if let Some((u, last_part)) = user.rsplit_once('.') && let Ok(num_val) = last_part.parse::<u16>() block) currently assigns agent = num_val as u8 without range-checking and can truncate values; add the same bounds check used in the later fallback branch (if num_val > u8::MAX as u16 { return Err(JidError::InvalidFormat(format!("Agent component out of range: {num_val}"))); }) before casting, and keep the assignments user = u and agent = num_val as u8 only after the check.src/handlers/iq.rs (1)
26-34:⚠️ Potential issue | 🔴 CriticalDereference
OwnedNodeRefbefore passing to IQ helpers.This handler treats
Arc<OwnedNodeRef>as aNode/NodeRef, causing three compile errors:handle_iq()expects&NodeRef<'_>,attrsrequires dereferencing via.get(), andDisplayableNodeexpects&Node.Fix
async fn handle(&self, client: Arc<Client>, node: Arc<wacore_binary::OwnedNodeRef>, _cancelled: &mut bool) -> bool { - if !client.handle_iq(&node).await { - if node.attrs.get("type").is_some_and(|s| s == "result") { + let node_ref = node.get(); + if !client.handle_iq(node_ref).await { + if node_ref.attrs.get("type").is_some_and(|s| s == "result") { + let owned = node.to_owned_node(); debug!( "Received late IQ response (waiter already removed): {}", - DisplayableNode(&node) + DisplayableNode(&owned) ); } else { - warn!("Received unhandled IQ: {}", DisplayableNode(&node)); + let owned = node.to_owned_node(); + warn!("Received unhandled IQ: {}", DisplayableNode(&owned)); } } true }Alternatively, use
DisplayableNodeRef(&node_ref)instead of converting to owned for better efficiency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/iq.rs` around lines 26 - 34, The handler is treating Arc<OwnedNodeRef> as a Node/NodeRef causing type errors; explicitly dereference the Arc<OwnedNodeRef> to a &NodeRef before calling helper APIs. Inside handle(), bind a reference like let node_ref = node.as_ref() or &*node and then call client.handle_iq(node_ref). Use node_ref.attrs.get(...) for attributes and pass node_ref to DisplayableNode (or use DisplayableNodeRef(node_ref) as suggested) so all three places (handle_iq, attrs.get, DisplayableNode) receive the correct referenced type.src/handlers/message.rs (1)
32-113:⚠️ Potential issue | 🔴 CriticalThis handler still mixes
OwnedNodeRefreads with anArc<Node>message queue.The code has two compile errors:
node.attrs()fails becauseOwnedNodeRefdoes not have anattrs()method; you must callnode.get().attrs()to access the innerNodeRef.tx.try_send(node)fails because the worker queue is typed asasync_channel::unbounded::<Arc<Node>>()but you are trying to sendArc<OwnedNodeRef>. You must convert:Arc::new(node.to_owned_node()).🐛 Minimal compatible fix
- let chat_id = match node.attrs().optional_jid("from") { + let chat_id = match node.get().attrs().optional_jid("from") { Some(jid) => jid.to_string(), None => { warn!("Message stanza missing required 'from' attribute"); return false; } @@ - if let Err(e) = tx.try_send(node) { + if let Err(e) = tx.try_send(Arc::new(node.to_owned_node())) { warn!("Failed to enqueue message for processing: {e}"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/message.rs` around lines 32 - 113, The handler mixes OwnedNodeRef and Arc<Node>: replace direct calls to node.attrs() with node.get().attrs() to access the inner NodeRef, and when enqueuing convert the OwnedNodeRef into the queue's Arc<Node> type (e.g., wrap the owned node: Arc::new(node.to_owned_node()) or whatever conversion method exists) before calling tx.try_send; update the code paths around handle, node.get(), attrs(), message_queues and tx.try_send to perform these swaps so types align.wacore/binary/src/decoder.rs (1)
131-144:⚠️ Potential issue | 🟠 MajorPreserve agent-coded PN
AD_JIDs on decode.The write path still emits
domain_type = jid.agentfor unmapped servers, so a PN AD_JID with a non-zero agent byte can be produced byserver_to_domain_type(). The new match rejects those values, which breaks encode/decode symmetry for otherwise valid JIDs.💡 Suggested fix
let server = match agent { - 0 => crate::jid::Server::Pn, 1 => crate::jid::Server::Lid, 128 => crate::jid::Server::Hosted, 129 => crate::jid::Server::HostedLid, n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, + n if (n & 128) == 0 => crate::jid::Server::Pn, _ => { return Err(BinaryError::AttrParse(format!( "AD_JID invalid domain type: {agent}" ))); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/decoder.rs` around lines 131 - 144, The decoder currently rejects unknown agent values for AD_JID, breaking encode/decode symmetry; change the fallback so unmapped agent bytes are treated as PN (preserving agent-coded PN AD_JID) instead of returning an error. In the match on `agent` (the AD_JID domain type parsing) return crate::jid::Server::Pn for the default/unknown-arm (or otherwise accept and map unmapped `agent` values to `Server::Pn`) so values emitted by `server_to_domain_type()` round-trip instead of erroring.src/handlers/presence.rs (1)
24-40:⚠️ Potential issue | 🔴 CriticalFix the
OwnedNodeRefaccess pattern before this can build.
Arc<OwnedNodeRef>does not exposeattrs; onlyOwnedNodeRef::get()returns the borrowedNodeRef, and from there you needget_attr()or.attrsfield access. This handler fails to compile withE0609.💡 Suggested fix
async fn handle(&self, client: Arc<Client>, node: Arc<wacore_binary::OwnedNodeRef>, _cancelled: &mut bool) -> bool { - let from_jid = match node.attrs.get("from").and_then(|v| v.to_jid()) { + let node_ref = node.get(); + let from_jid = match node_ref.get_attr("from").and_then(|v| v.to_jid()) { Some(jid) => jid, None => { debug!(target: "PresenceHandler", "Presence stanza missing or invalid 'from' attribute"); return true; } }; - let unavailable = node.attrs.get("type").is_some_and(|v| v == "unavailable"); + let unavailable = node_ref + .get_attr("type") + .and_then(|v| v.as_str()) + .is_some_and(|v| v == "unavailable"); // Parse last_seen from 'last' attribute if present let last_seen = node - .attrs - .get("last") - .and_then(|v| v.as_str().parse::<i64>().ok()) + _ref.get_attr("last") + .and_then(|v| v.as_str()) + .and_then(|v| v.parse::<i64>().ok()) .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/presence.rs` around lines 24 - 40, The handler uses Arc<wacore_binary::OwnedNodeRef> (parameter name node) as if it exposes attrs directly, causing E0609; change accesses to call node.get() to obtain the borrowed NodeRef and then use NodeRef methods (e.g., get_attr("from") / get_attr("type") / get_attr("last") or access the .attrs on the borrowed NodeRef) when parsing from_jid, unavailable, and last_seen inside async fn handle; update all occurrences referencing node.attrs to use node.get().get_attr(...) (or node.get().attrs) and adjust the last_seen parsing to parse the string from the borrowed attribute value accordingly.src/client.rs (9)
3100-3105:⚠️ Potential issue | 🔴 CriticalCompilation error: No
attrs()method inhandle_connect_failure.Same issue as
handle_stream_error— useattr_parser()instead.Proposed fix
pub(crate) async fn handle_connect_failure(&self, node: &wacore_binary::NodeRef<'_>) { self.expected_disconnect.store(true, Ordering::Relaxed); self.shutdown_notifier.notify(usize::MAX); - let mut attrs = node.attrs(); + let mut attrs = node.attr_parser(); let reason_code = attrs.optional_u64("reason").unwrap_or(0) as i32;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3100 - 3105, In handle_connect_failure replace the call to node.attrs() with node.attr_parser() (same fix as handle_stream_error): obtain the attribute parser via let mut attrs = node.attr_parser(); and then use attrs.optional_u64("reason").unwrap_or(0) as i32 to read the reason code, ensuring the parser API is used instead of the non-existent attrs() method.
1412-1416:⚠️ Potential issue | 🔴 CriticalCompilation error:
OwnedNodeRefhas notagfield.At this point,
nodeis anOwnedNodeRefreturned fromdecrypt_frame. To accesstag, you need to call.get()first to obtain the innerNodeRef.Proposed fix
// - Everything else: spawned concurrently for parallelism let process_inline = matches!( - node.tag.as_ref(), + node.get().tag.as_ref(), "success" | "failure" | "stream:error" | "message" | "ib" );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1412 - 1416, The code attempts to access node.tag on an OwnedNodeRef returned by decrypt_frame, causing a compilation error; update the matches call to use node.get().tag (i.e., call .get() on the OwnedNodeRef before inspecting tag) so process_inline is computed from node.get().tag, ensuring you dereference the OwnedNodeRef returned by decrypt_frame when checking tags like "success" | "failure" | "stream:error" | "message" | "ib".
1926-1941:⚠️ Potential issue | 🔴 CriticalCompilation error: Cannot index
node.attrswithget()onNodeRef.
NodeRef.attrsis a slice[(Cow<'_, str>, ValueRef<'_>)], not aHashMap. Theget("lid")call doesn't work. Useget_attr("lid")or iterate to find the attribute.Proposed fix
// Extract LID from the node before spawning (node isn't Send). - let lid_from_server = match node.attrs.get("lid") { - Some(lid_value) => match lid_value.to_jid() { + let lid_from_server = match node.get_attr("lid") { + Some(lid_value) => match lid_value.to_jid() { Some(lid) => Some(lid), None => { - warn!("Failed to parse LID from success stanza: {lid_value}"); + warn!("Failed to parse LID from success stanza: {:?}", lid_value); None } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1926 - 1941, The code attempts to index node.attrs with get("lid") which fails because NodeRef.attrs is a slice of (Cow<str>, ValueRef) pairs; change the extraction in the lid_from_server block to use the NodeRef API or iterate the slice: call node.get_attr("lid") if available or iterate node.attrs to find the tuple whose 0 equals "lid", then convert its ValueRef to_jid() as before; update the lid_from_server match to use that retrieved attribute (refer to symbols: node.attrs, node.get_attr, lid_from_server, self.update_server_time_offset) and preserve the same logging paths when attribute is missing or parsing fails.
1632-1651:⚠️ Potential issue | 🔴 CriticalMultiple type mismatches:
Arc<OwnedNodeRef>vsArc<Node>.The
Event::RawNodevariant andresolve_node_waitersfunction still expectArc<Node>, butnodeis nowArc<OwnedNodeRef>. Either:
- Update the
Event::RawNodetype andNodeWaiter/resolve_node_waitersto useArc<OwnedNodeRef>, or- Convert to owned
Nodebefore passing (less efficient, defeats zero-copy goal)The same issue applies to
maybe_deferred_ackat line 1682 which expectsArc<Node>.Proposed fix (option 1 - update signatures)
Update
NodeWaiterandresolve_node_waiters:struct NodeWaiter { filter: NodeFilter, - tx: futures::channel::oneshot::Sender<Arc<Node>>, + tx: futures::channel::oneshot::Sender<Arc<wacore_binary::OwnedNodeRef>>, } fn resolve_waiters( waiters_mutex: &std::sync::Mutex<Vec<NodeWaiter>>, counter: &AtomicUsize, - node: &Arc<Node>, + node: &Arc<wacore_binary::OwnedNodeRef>, ) {Update
NodeFilter::matchesto takeNodeRef:- fn matches(&self, node: &Node) -> bool { - node.tag == self.tag + fn matches(&self, node: &wacore_binary::NodeRef<'_>) -> bool { + node.tag.as_ref() == self.tag && self .attrs .iter() - .all(|(k, v)| node.attrs.get(k.as_str()).is_some_and(|attr| *attr == *v)) + .all(|(k, v)| node.get_attr(k).is_some_and(|attr| attr.as_str() == Some(v.as_str()))) }Update
maybe_deferred_acksignature andsend_ack_forto convert:- async fn maybe_deferred_ack(self: &Arc<Self>, node: Arc<Node>) { + async fn maybe_deferred_ack(self: &Arc<Self>, node: Arc<wacore_binary::OwnedNodeRef>) { if self.synchronous_ack { - if let Err(e) = self.send_ack_for(&node).await { + if let Err(e) = self.send_ack_for(&node.to_owned_node()).await {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1632 - 1651, The code currently passes an Arc<OwnedNodeRef> (variable node) to places expecting Arc<Node>, causing type mismatches; update the types to accept OwnedNodeRef to preserve zero-copy: change Event::RawNode variant, the NodeWaiter type and resolve_node_waiters(&node) signature to accept Arc<OwnedNodeRef> (or NodeRef/OwnedNodeRef as used in your codebase), update NodeFilter::matches to take a NodeRef/OwnedNodeRef instead of Arc<Node>, and update maybe_deferred_ack and send_ack_for signatures/usages to accept Arc<OwnedNodeRef> (converting to owned Node only where strictly necessary). Ensure all call sites that dispatch Event::RawNode, invoke resolve_node_waiters, call NodeFilter::matches, and call maybe_deferred_ack/send_ack_for are updated accordingly to the new OwnedNodeRef-based types.
2982-2997:⚠️ Potential issue | 🔴 CriticalCompilation error: No
attrs()method onNodeRef.
NodeRefdoesn't have anattrs()method. Based on the code pattern, you should useattr_parser()to get anAttrParseror access theattrsfield directly and iterate.Proposed fix
pub(crate) async fn handle_stream_error(&self, node: &wacore_binary::NodeRef<'_>) { self.is_logged_in.store(false, Ordering::Relaxed); - let mut attrs = node.attrs(); - let code_cow = attrs.optional_string("code"); + let mut attrs = node.attr_parser(); + let code_cow = attrs.optional_string("code"); let code = code_cow.as_deref().unwrap_or(""); let conflict_type = node .get_optional_child("conflict") .map(|n| { - n.attrs() + n.attr_parser() .optional_string("type") .as_deref() .unwrap_or("") .to_string() }) .unwrap_or_default();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 2982 - 2997, The compile error occurs because wacore_binary::NodeRef has no attrs() method; update handle_stream_error to use node.attr_parser() (or access the attr_parser field) instead: replace let mut attrs = node.attrs() with let mut parser = node.attr_parser(); then get the code via parser.optional_string("code").as_deref().unwrap_or(""); similarly, when mapping the "conflict" child, call n.attr_parser().optional_string("type").as_deref().unwrap_or("").to_string() so all attribute parsing uses AttrParser methods instead of attrs(); keep existing variable names (code_cow/code and conflict_type) and behavior otherwise.
3698-3731:⚠️ Potential issue | 🔴 CriticalTest compilation errors:
should_acksignature changed.The tests at lines 3719-3725 call
client.should_ack(&receipt_node)andclient.should_ack(¬ification_node)with&Node, butshould_acknow takes&NodeRef. The tests need to be updated to work with the new API.Proposed fix
Either:
- Create
OwnedNodeReffrom marshaled bytes and callshould_ack(owned.get())- Add a helper method that accepts
&Nodefor testing- Update the test to use the new types
+ // Helper to check should_ack with Node (for tests) + fn should_ack_node(client: &Client, node: &Node) -> bool { + let bytes = wacore_binary::marshal::marshal_auto(node).unwrap(); + let owned = wacore_binary::OwnedNodeRef::new(bytes).unwrap(); + client.should_ack(owned.get()) + } + assert!( - client.should_ack(&receipt_node), + should_ack_node(&client, &receipt_node), "should_ack must still return TRUE for <receipt> stanzas." ); assert!( - client.should_ack(¬ification_node), + should_ack_node(&client, ¬ification_node), "should_ack must still return TRUE for <notification> stanzas." );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3698 - 3731, The tests call client.should_ack(&receipt_node) and ¬ification_node but should_ack now expects &NodeRef; update the test to create OwnedNodeRef instances from the existing Node values (marshal/encode each Node into bytes and construct an OwnedNodeRef), then call client.should_ack(owned_ref.get()) for both receipt and notification nodes and assert the results as before; reference symbols: should_ack, OwnedNodeRef, NodeRef, client, receipt_node, notification_node.
3157-3178:⚠️ Potential issue | 🔴 CriticalMultiple compilation errors in
handle_iq: attrs indexing and downstream call.
node.attrs.get("type")doesn't work onNodeRefslice — useget_attr("type")node.attrs.get("xmlns")— same issuenode.attrs()call — useattr_parser()pair::handle_iq(self, node)expects&Nodebut receives&NodeRef— thepairmodule needs updating tooProposed fix
pub(crate) async fn handle_iq(self: &Arc<Self>, node: &wacore_binary::NodeRef<'_>) -> bool { - if node.attrs.get("type").is_some_and(|s| s == "get") + if node.get_attr("type").is_some_and(|s| s == "get") && (node.get_optional_child("ping").is_some() || node - .attrs - .get("xmlns") + .get_attr("xmlns") .is_some_and(|s| s == "urn:xmpp:ping")) { info!("Received ping, sending pong."); - let mut parser = node.attrs(); + let mut parser = node.attr_parser(); let from_jid = parser.jid("from"); let id = parser.optional_string("id").map(|s| s.to_string()); let pong = build_pong(from_jid.to_string(), id.as_deref()); if let Err(e) = self.send_node(pong).await { warn!("Failed to send pong: {e:?}"); } return true; } // Pass Node directly to pair handling - if pair::handle_iq(self, node).await { + if pair::handle_iq(self, &node.to_owned_node()).await { return true; }Note: Converting to owned node for
pair::handle_iqis a workaround. Ideally,pair::handle_iqshould also be updated to accept&NodeRef.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3157 - 3178, In handle_iq replace the incorrect attrs accessors: use node.get_attr("type") and node.get_attr("xmlns") instead of node.attrs.get(...), and replace node.attrs() with node.attr_parser() when building the parser used to extract from_jid and id (so keep build_pong(from_jid.to_string(), id.as_deref()) and send_node usage unchanged). For the pair::handle_iq call either convert the NodeRef into an owned Node before calling pair::handle_iq(self, owned_node).await or update the pair::handle_iq signature to accept &wacore_binary::NodeRef<'_> so the call matches; ensure the chosen approach is applied consistently in this module and the pair module (referencing handle_iq, build_pong, attr_parser, and pair::handle_iq).
4887-4959:⚠️ Potential issue | 🔴 CriticalTest compilation errors:
process_nodesignature changed.Multiple tests (e.g.,
test_ib_thread_metadata_does_not_end_sync,test_ib_offline_child_ends_sync) callclient.process_node(Arc::new(node))wherenodeis aNode, butprocess_nodenow expectsArc<OwnedNodeRef>.Proposed fix
Create a helper function or update each test:
+ /// Helper to convert Node to OwnedNodeRef for testing + fn node_to_owned_ref(node: Node) -> wacore_binary::OwnedNodeRef { + let bytes = wacore_binary::marshal::marshal_auto(&node).unwrap(); + wacore_binary::OwnedNodeRef::new(bytes).unwrap() + } + #[tokio::test] async fn test_ib_thread_metadata_does_not_end_sync() { let client = create_offline_sync_test_client().await; client .offline_sync_metrics .active .store(true, Ordering::Release); let node = NodeBuilder::new("ib") .children([NodeBuilder::new("thread_metadata") .children([NodeBuilder::new("item").build()]) .build()]) .build(); - client.process_node(Arc::new(node)).await; + client.process_node(Arc::new(node_to_owned_ref(node))).await;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4887 - 4959, Tests call client.process_node(Arc::new(node)) but process_node now expects Arc<OwnedNodeRef>, so update the tests (e.g., in test_ib_thread_metadata_does_not_end_sync, test_ib_edge_routing_does_not_end_sync, test_ib_dirty_does_not_end_sync, test_ib_offline_child_ends_sync) to convert the Node built by NodeBuilder into an OwnedNodeRef before wrapping in Arc; for example, construct the Node with NodeBuilder, convert it to an OwnedNodeRef (using the crate’s conversion API such as OwnedNodeRef::from(node) or the provided Node->OwnedNodeRef constructor), then call client.process_node(Arc::new(owned_node_ref)). Ensure references to process_node, OwnedNodeRef, NodeBuilder, and create_offline_sync_test_client are updated accordingly.
3070-3078:⚠️ Potential issue | 🔴 CriticalCompilation error:
DisplayableNodeexpects&Node, not&NodeRef.Multiple locations use
DisplayableNode(node)butnodeis now&NodeRef. The code already importsDisplayableNodeRefat line 1528 — use that instead. Also,StreamError.rawexpectsOption<Node>, notNodeRef.Proposed fix
- _ => { - error!("Unknown stream error: {}", DisplayableNode(node)); + _ => { + error!("Unknown stream error: {}", DisplayableNodeRef(node)); self.expected_disconnect.store(true, Ordering::Relaxed); self.core.event_bus.dispatch(&Event::StreamError( crate::types::events::StreamError { code: code.to_string(), - raw: Some(node.clone()), + raw: Some(node.to_owned_node()), }, )); }Apply similar changes at lines 3129, 3142, and 3151.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3070 - 3078, Replace uses of DisplayableNode(node) with DisplayableNodeRef(node) because node is a &NodeRef; also ensure StreamError.raw is given an owned Node by converting the NodeRef (e.g., Some(node.to_owned()) or the appropriate to_owned/clone API) instead of passing the NodeRef directly. Update the block creating crate::types::events::StreamError (and the other similar sites using DisplayableNode(node)) to use DisplayableNodeRef and to pass raw: Some(node.to_owned()) so the type matches Option<Node>.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client.rs`:
- Around line 3265-3267: The call here passes a &wacore_binary::NodeRef but
unified_session.update_server_time_offset expects a &Node, so either change
UnifiedSessionManager::update_server_time_offset to accept
&wacore_binary::NodeRef<'_> (update its signature and usages) or convert the
NodeRef to the owned Node before calling
unified_session.update_server_time_offset (e.g., obtain an owned Node via the
appropriate conversion/clone method on NodeRef and pass a &Node). Target
symbols: update_server_time_offset (in this file),
unified_session.update_server_time_offset,
UnifiedSessionManager::update_server_time_offset, and wacore_binary::NodeRef.
- Around line 1687-1693: The failure comes from a mismatch between should_ack's
signature and how it's used: should_ack currently takes
&wacore_binary::NodeRef<'_> (function should_ack) but callers/tests pass &Node;
either change should_ack to accept &wacore_binary::Node (if that API is easier)
or update the callers/tests to pass a NodeRef by converting the Node to a
NodeRef (e.g. node.as_ref() / node.to_ref() / Node::as_ref() depending on the
crate API) and ensure attribute checks use the correct accessor for NodeRef
(e.g. get_attr(...) -> Option<&ValueRef> or attr(...) as provided by the
library); pick one consistent approach and update all usages/tests to match
should_ack and the Node/NodeRef attribute accessor.
In `@src/client/device_registry.rs`:
- Around line 307-313: The code silently downgrades failed Server conversion to
Server::Pn causing LID sessions to be skipped; replace the
try_from(...).unwrap_or(Server::Pn) fallback by iterating the Server enum
variants explicitly (so both LID and PN are handled) when building the Jid
inside the loops; locate the loop using variables servers, lookup.all_keys(),
device_ids and the conversion call wacore_binary::jid::Server::try_from(srv) /
Server::Pn and change it to enumerate the actual Server variants (or otherwise
obtain both relevant Server variants) before calling Jid::new and setting
jid.device so delete_sessions_for_devices() will run for each server variant
rather than masking conversion failures.
In `@src/handlers/chatstate.rs`:
- Around line 57-58: The handler is passing an Arc<OwnedNodeRef> to
ChatstateStanza::parse which expects &Node; change the call to pass the
converted node by invoking to_owned_node() on the Arc (e.g., let owned =
node.to_owned_node(); then call ChatstateStanza::parse(&owned)) so parse
receives a &Node instead of &Arc<OwnedNodeRef>.
In `@src/handlers/ib.rs`:
- Around line 29-30: The helper handle_ib_impl must be migrated from taking
&Node to the zero-copy type: change its signature to accept
Arc<wacore_binary::OwnedNodeRef> (e.g. async fn handle_ib_impl(client:
Arc<Client>, node: Arc<wacore_binary::OwnedNodeRef>) -> ...), update any
internal code in handle_ib_impl that references Node to use
wacore_binary::OwnedNodeRef, and update the call sites in this file (currently
calling handle_ib_impl(client, &node)) to pass the Arc (e.g.
handle_ib_impl(client, node.clone()) or handle_ib_impl(client, node) as
appropriate). Also adjust imports to bring wacore_binary::OwnedNodeRef into
scope.
In `@src/handlers/notification.rs`:
- Around line 37-39: The handler method handle currently passes an
Arc<wacore_binary::OwnedNodeRef> into handle_notification_impl which expects a
&Node; convert the yoke-backed stanza to an owned Node at the boundary by
calling the to_owned_node() method on the OwnedNodeRef before awaiting
handle_notification_impl. Locate the handle function and change the argument
from &node to a temporary owned_node produced via node.to_owned_node() (or
similar) and pass &owned_node into handle_notification_impl so types match.
In `@src/handlers/receipt.rs`:
- Around line 23-24: The handler currently passes
Arc<wacore_binary::OwnedNodeRef> (named node) into client.handle_receipt, but
Client::handle_receipt expects Arc<Node>, causing the E0308; fix by converting
the OwnedNodeRef to the expected Arc<Node> before calling client.handle_receipt
(or alternatively change Client::handle_receipt to accept
Arc<wacore_binary::OwnedNodeRef> to finish the migration). Locate the async fn
handle in src/handlers/receipt.rs and either (a) transform node into an
Arc<Node> (using the appropriate conversion/constructor provided by
wacore_binary, e.g., a from/into/to_node helper) and pass that to
client.handle_receipt, or (b) update the signature of Client::handle_receipt to
accept Arc<wacore_binary::OwnedNodeRef> and adjust its implementation
accordingly so types match.
In `@src/handlers/router.rs`:
- Around line 49-56: Tests and MockHandler need to be updated to use the new
zero-copy OwnedNodeRef type: change the MockHandler impl and test fixtures that
currently accept Arc<Node> to accept Arc<wacore_binary::OwnedNodeRef> (so they
match dispatch signature and handlers.get(...).handle(client, node,
cancelled).await), and update the dispatch tests to construct and pass
Arc<OwnedNodeRef> instances instead of Arc<Node> when calling Router::dispatch;
ensure any pattern matching or field access in MockHandler uses node.get() like
the production code.
In `@src/handlers/unimplemented.rs`:
- Around line 46-47: In handle (async fn handle) replace direct access to
node.tag with the owned node's borrowed NodeRef: call node.get() to obtain the
NodeRef, then read the tag via node_ref.tag.as_ref() and pass that &str into
client.handle_unimplemented(...). Concretely, inside handle use let nr =
node.get(); then await client.handle_unimplemented(nr.tag.as_ref()).await so you
respect OwnedNodeRef's API and convert the Cow<str> to &str.
In `@wacore/binary/src/decoder.rs`:
- Around line 111-114: The code currently coerces an invalid JID server into
Server::Pn; instead, when crate::jid::Server::try_from(server_str.as_ref())
fails (i.e., server_str is missing/unknown), return or propagate a decoding
error rather than defaulting to Server::Pn. Locate the server parsing in
decoder.rs (the read_value_as_string()/server_str and Server::try_from usage)
and replace the unwrap_or(...Server::Pn) path with proper error handling: map
the try_from failure into the decoder's Result error (or use ? to propagate) so
malformed JID_PAIR server values fail fast.
In `@wacore/binary/src/jid.rs`:
- Around line 296-298: The is_ad method currently checks server() against
Server::Pn, Server::Lid, and Server::Hosted but omits Server::HostedLid, causing
`@hosted.lid` device JIDs to be misclassified; update the match in fn is_ad(&self)
-> bool (and any similar checks) to include Server::HostedLid alongside Pn, Lid,
and Hosted so hosted-LID device JIDs are recognized as AD.
In `@wacore/src/iq/privacy.rs`:
- Line 638: The test fixture currently uses a phone-like PN JID via pn_jid:
Some(Jid::new("5511999999999", Server::Pn)) which appears real; update the
fixture to use an obviously fictitious/reserved identifier (e.g., a clearly fake
phone/JID string) when constructing the pn_jid with Jid::new and keep Server::Pn
unchanged so tests avoid real PII—locate the pn_jid field in
wacore/src/iq/privacy.rs and replace the literal with a clearly fake value.
---
Outside diff comments:
In `@src/client.rs`:
- Around line 3100-3105: In handle_connect_failure replace the call to
node.attrs() with node.attr_parser() (same fix as handle_stream_error): obtain
the attribute parser via let mut attrs = node.attr_parser(); and then use
attrs.optional_u64("reason").unwrap_or(0) as i32 to read the reason code,
ensuring the parser API is used instead of the non-existent attrs() method.
- Around line 1412-1416: The code attempts to access node.tag on an OwnedNodeRef
returned by decrypt_frame, causing a compilation error; update the matches call
to use node.get().tag (i.e., call .get() on the OwnedNodeRef before inspecting
tag) so process_inline is computed from node.get().tag, ensuring you dereference
the OwnedNodeRef returned by decrypt_frame when checking tags like "success" |
"failure" | "stream:error" | "message" | "ib".
- Around line 1926-1941: The code attempts to index node.attrs with get("lid")
which fails because NodeRef.attrs is a slice of (Cow<str>, ValueRef) pairs;
change the extraction in the lid_from_server block to use the NodeRef API or
iterate the slice: call node.get_attr("lid") if available or iterate node.attrs
to find the tuple whose 0 equals "lid", then convert its ValueRef to_jid() as
before; update the lid_from_server match to use that retrieved attribute (refer
to symbols: node.attrs, node.get_attr, lid_from_server,
self.update_server_time_offset) and preserve the same logging paths when
attribute is missing or parsing fails.
- Around line 1632-1651: The code currently passes an Arc<OwnedNodeRef>
(variable node) to places expecting Arc<Node>, causing type mismatches; update
the types to accept OwnedNodeRef to preserve zero-copy: change Event::RawNode
variant, the NodeWaiter type and resolve_node_waiters(&node) signature to accept
Arc<OwnedNodeRef> (or NodeRef/OwnedNodeRef as used in your codebase), update
NodeFilter::matches to take a NodeRef/OwnedNodeRef instead of Arc<Node>, and
update maybe_deferred_ack and send_ack_for signatures/usages to accept
Arc<OwnedNodeRef> (converting to owned Node only where strictly necessary).
Ensure all call sites that dispatch Event::RawNode, invoke resolve_node_waiters,
call NodeFilter::matches, and call maybe_deferred_ack/send_ack_for are updated
accordingly to the new OwnedNodeRef-based types.
- Around line 2982-2997: The compile error occurs because wacore_binary::NodeRef
has no attrs() method; update handle_stream_error to use node.attr_parser() (or
access the attr_parser field) instead: replace let mut attrs = node.attrs() with
let mut parser = node.attr_parser(); then get the code via
parser.optional_string("code").as_deref().unwrap_or(""); similarly, when mapping
the "conflict" child, call
n.attr_parser().optional_string("type").as_deref().unwrap_or("").to_string() so
all attribute parsing uses AttrParser methods instead of attrs(); keep existing
variable names (code_cow/code and conflict_type) and behavior otherwise.
- Around line 3698-3731: The tests call client.should_ack(&receipt_node) and
¬ification_node but should_ack now expects &NodeRef; update the test to
create OwnedNodeRef instances from the existing Node values (marshal/encode each
Node into bytes and construct an OwnedNodeRef), then call
client.should_ack(owned_ref.get()) for both receipt and notification nodes and
assert the results as before; reference symbols: should_ack, OwnedNodeRef,
NodeRef, client, receipt_node, notification_node.
- Around line 3157-3178: In handle_iq replace the incorrect attrs accessors: use
node.get_attr("type") and node.get_attr("xmlns") instead of node.attrs.get(...),
and replace node.attrs() with node.attr_parser() when building the parser used
to extract from_jid and id (so keep build_pong(from_jid.to_string(),
id.as_deref()) and send_node usage unchanged). For the pair::handle_iq call
either convert the NodeRef into an owned Node before calling
pair::handle_iq(self, owned_node).await or update the pair::handle_iq signature
to accept &wacore_binary::NodeRef<'_> so the call matches; ensure the chosen
approach is applied consistently in this module and the pair module (referencing
handle_iq, build_pong, attr_parser, and pair::handle_iq).
- Around line 4887-4959: Tests call client.process_node(Arc::new(node)) but
process_node now expects Arc<OwnedNodeRef>, so update the tests (e.g., in
test_ib_thread_metadata_does_not_end_sync,
test_ib_edge_routing_does_not_end_sync, test_ib_dirty_does_not_end_sync,
test_ib_offline_child_ends_sync) to convert the Node built by NodeBuilder into
an OwnedNodeRef before wrapping in Arc; for example, construct the Node with
NodeBuilder, convert it to an OwnedNodeRef (using the crate’s conversion API
such as OwnedNodeRef::from(node) or the provided Node->OwnedNodeRef
constructor), then call client.process_node(Arc::new(owned_node_ref)). Ensure
references to process_node, OwnedNodeRef, NodeBuilder, and
create_offline_sync_test_client are updated accordingly.
- Around line 3070-3078: Replace uses of DisplayableNode(node) with
DisplayableNodeRef(node) because node is a &NodeRef; also ensure StreamError.raw
is given an owned Node by converting the NodeRef (e.g., Some(node.to_owned()) or
the appropriate to_owned/clone API) instead of passing the NodeRef directly.
Update the block creating crate::types::events::StreamError (and the other
similar sites using DisplayableNode(node)) to use DisplayableNodeRef and to pass
raw: Some(node.to_owned()) so the type matches Option<Node>.
In `@src/handlers/iq.rs`:
- Around line 26-34: The handler is treating Arc<OwnedNodeRef> as a Node/NodeRef
causing type errors; explicitly dereference the Arc<OwnedNodeRef> to a &NodeRef
before calling helper APIs. Inside handle(), bind a reference like let node_ref
= node.as_ref() or &*node and then call client.handle_iq(node_ref). Use
node_ref.attrs.get(...) for attributes and pass node_ref to DisplayableNode (or
use DisplayableNodeRef(node_ref) as suggested) so all three places (handle_iq,
attrs.get, DisplayableNode) receive the correct referenced type.
In `@src/handlers/message.rs`:
- Around line 32-113: The handler mixes OwnedNodeRef and Arc<Node>: replace
direct calls to node.attrs() with node.get().attrs() to access the inner
NodeRef, and when enqueuing convert the OwnedNodeRef into the queue's Arc<Node>
type (e.g., wrap the owned node: Arc::new(node.to_owned_node()) or whatever
conversion method exists) before calling tx.try_send; update the code paths
around handle, node.get(), attrs(), message_queues and tx.try_send to perform
these swaps so types align.
In `@src/handlers/presence.rs`:
- Around line 24-40: The handler uses Arc<wacore_binary::OwnedNodeRef>
(parameter name node) as if it exposes attrs directly, causing E0609; change
accesses to call node.get() to obtain the borrowed NodeRef and then use NodeRef
methods (e.g., get_attr("from") / get_attr("type") / get_attr("last") or access
the .attrs on the borrowed NodeRef) when parsing from_jid, unavailable, and
last_seen inside async fn handle; update all occurrences referencing node.attrs
to use node.get().get_attr(...) (or node.get().attrs) and adjust the last_seen
parsing to parse the string from the borrowed attribute value accordingly.
In `@wacore/binary/src/decoder.rs`:
- Around line 131-144: The decoder currently rejects unknown agent values for
AD_JID, breaking encode/decode symmetry; change the fallback so unmapped agent
bytes are treated as PN (preserving agent-coded PN AD_JID) instead of returning
an error. In the match on `agent` (the AD_JID domain type parsing) return
crate::jid::Server::Pn for the default/unknown-arm (or otherwise accept and map
unmapped `agent` values to `Server::Pn`) so values emitted by
`server_to_domain_type()` round-trip instead of erroring.
In `@wacore/binary/src/jid.rs`:
- Around line 705-710: The Display implementations for JID append ".{agent}"
when self.agent > 0 but currently skip only Server::Pn, Server::Lid, and
Server::Hosted; update the condition to also skip Server::HostedLid so the agent
is not appended for hosted-lid addresses (preventing outputs like
user.<agent>:<device>@hosted.lid); locate the branches that check
matches!(self.server, Server::Pn | Server::Lid | Server::Hosted) in the Display
impl(s) (the blocks that call write!(f, ".{}", self.agent)?), add
Server::HostedLid to that matches! list in both places (the other occurrence
around the 723-731 range) so hosted-lid follows the same suppression logic.
- Around line 652-685: The early branch that handles dotted-agent when server !=
DEFAULT_USER_SERVER && server != HIDDEN_USER_SERVER (the if let Some((u,
last_part)) = user.rsplit_once('.') && let Ok(num_val) =
last_part.parse::<u16>() block) currently assigns agent = num_val as u8 without
range-checking and can truncate values; add the same bounds check used in the
later fallback branch (if num_val > u8::MAX as u16 { return
Err(JidError::InvalidFormat(format!("Agent component out of range:
{num_val}"))); }) before casting, and keep the assignments user = u and agent =
num_val as u8 only after the check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 639eff63-3fd9-4b0c-93cf-868532187e07
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
Cargo.tomlsrc/client.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/message.rssrc/pair_code.rssrc/request.rssrc/send.rssrc/usync.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/encoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/prekeys.rswacore/src/types/jid.rswacore/tests/jid_test.rs
f457025 to
2b4beb4
Compare
2b4beb4 to
ab548ea
Compare
|
@claude review, check if all issues pointed out by reviews are solved, check for more bugs, inconsistencies, and more performance improvement opportunities, please do a careful and deep review |
|
Claude finished @jlucaso1's task in 9m 51s —— View job Deep Review: Yoke Zero-Copy & Server Enum PR
Previously Reported Issues — Resolution StatusAll issues flagged by CodeRabbit and Codex in the previous round have been addressed:
New FindingsBug: Redundant fallback parser block (jid.rs:675-690)The
For non-default servers, Block 2 re-parses from For Recommendation: Remove Block 2 entirely — it is redundant and a latent bug source. Fix this → Performance:
|
| Location | Handler | Severity | Why |
|---|---|---|---|
src/handlers/message.rs:117 |
MessageHandler | High | Every incoming message is deep-cloned. Channel type is still Sender<Arc<Node>> (line 70) |
src/handlers/receipt.rs:28 |
ReceiptHandler | Medium | Every receipt: Arc::new(node.to_owned_node()) — creates a redundant Arc too |
src/handlers/notification.rs:43 |
NotificationHandler | Medium | Every notification deep-cloned |
src/client.rs:113 |
resolve_waiters |
Low | Only when IQ waiters are registered |
src/client.rs:1661 |
response_waiters |
Low | Only for IQ responses |
src/client.rs:1637 |
Event::RawNode |
Low | Gated by raw_node_forwarding flag (default off) |
The root cause is that downstream consumers (handle_incoming_message, handle_receipt, handle_notification_impl, NodeWaiter, Event::RawNode) still accept Node/Arc<Node> rather than OwnedNodeRef/NodeRef. Migrating these in a follow-up would unlock the full zero-copy benefit.
Highest-impact follow-up: Change message_queues channel type from Sender<Arc<Node>> to Sender<Arc<OwnedNodeRef>> and update handle_incoming_message to accept Arc<OwnedNodeRef>. This would eliminate the deepest allocation on the hottest path.
Performance: Redundant Server::try_from in production code (lid_pn.rs:150-151)
server: wacore_binary::jid::Server::try_from(lid_server)
.unwrap_or(wacore_binary::jid::Server::Lid),lid_server is the constant "lid" which always maps to Server::Lid. This try_from + unwrap_or is unnecessary runtime overhead in production code. Should just be server: Server::Lid. Same pattern appears 6 times in test code (src/message.rs:3585-3819) where it's less critical but still noisy. Fix this →
Inconsistency: Mixed string-constant vs enum comparisons
Several files still compare jid.server against string constants (DEFAULT_USER_SERVER, HIDDEN_USER_SERVER) via the PartialEq<str> impl, instead of using the Server enum directly. This works correctly but is inconsistent with the PR's goal:
src/message.rs:1310-1324—sender.server == lid_server/sender.server == pn_serversrc/client/lid_pn.rs:139-145— same patternwacore/src/prekeys.rs:113-114—jid.server == DEFAULT_USER_SERVER
These should use sender.server == Server::Lid or sender.is_lid() for type safety and clarity. Not a bug, but a missed migration opportunity.
Minor: is_empty() only checks user (jid.rs:342-344)
Codex flagged this as a potential regression. After review, this is not a regression — with the Server enum, there's no concept of an "empty server" (it always defaults to Server::Pn). Any Jid with user="" is semantically "empty" (a server-only JID). The build_iq_node target emission gated on !target.is_empty() still works correctly because server-only targets like Jid::new("", Server::Pn) will correctly report as empty and won't emit a redundant target attribute.
Architecture Observations
Yoke design is sound
The OwnedNodeRef wrapping Yoke<NodeRef<'static>, Vec<u8>> is correct:
NodeRef,ValueRef,NodeContentRef,JidRefall deriveYokeableVec<u8>implementsStableDeref(required by yoke)OwnedNodeRefisSend + Sync(required forArcsharing across async tasks)get()returns&NodeRef<'_>with correctly elided lifetime
Handler migration is well-structured
The tier approach is clear:
- Fully zero-copy (no
to_owned_node()):iq.rs,presence.rs,unimplemented.rs,basic.rs(Success/Failure/StreamError) - Bridge layer (
to_owned_node()at boundary):message.rs,receipt.rs,notification.rs,chatstate.rs,ib.rs,basic.rs(Ack)
send_ack_for is fully zero-copy
The ack path (maybe_deferred_ack → send_ack_for → build_ack_node) now takes &NodeRef throughout (client.rs:1700-1728). The Arc<OwnedNodeRef> is moved into the spawned async closure, keeping the buffer alive without cloning. This is the right pattern.
Summary
The PR is well-designed and nearly complete. All previously reported issues are resolved. The Server enum change is clean and complete. The yoke integration is architecturally sound with correct usage patterns.
Should fix before merge:
- Remove redundant Block 2 in JID fallback parser (jid.rs:675-690) — latent bug
- Replace
Server::try_from(lid_server).unwrap_or(Server::Lid)withServer::Lidin production code (lid_pn.rs:150-151)
Nice to have (follow-up):
- Migrate
message_queueschannel toSender<Arc<OwnedNodeRef>>for full zero-copy on the hottest path - Replace string-constant server comparisons with enum comparisons across message.rs, lid_pn.rs, prekeys.rs
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
wacore/binary/src/decoder.rs (1)
109-184: 🧹 Nitpick | 🔵 TrivialAdd regression coverage for the new server-decoding branches.
read_jid_pair,read_ad_jid,read_interop_jid, andread_fb_jidnow all have distinct malformed-server behavior, but this module still has no tests for any of those paths. A small table-driven set here would make the enum migration much safer to evolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/decoder.rs` around lines 109 - 184, Tests are missing for the new server-decoding branches; add table-driven unit tests that exercise malformed and valid-server paths for read_jid_pair, read_ad_jid, read_interop_jid, and read_fb_jid. For each function create inputs that produce: successful JidRef (when server string matches expected), InvalidNode (when missing/empty user or wrong interop/messenger server), and AttrParse (for read_jid_pair when server string fails Server::try_from), assert the returned Result matches BinaryError::InvalidNode or BinaryError::AttrParse or the expected JidRef fields; reference the decoder methods (read_jid_pair, read_ad_jid, read_interop_jid, read_fb_jid) and constants crate::jid::INTEROP_SERVER and crate::jid::MESSENGER_SERVER to build the test cases so regressions in server-decoding are caught.src/client.rs (1)
96-114: 🧹 Nitpick | 🔵 TrivialBroadcast the owned waiter payload instead of re-cloning it per waiter.
node.to_owned_node()is currently called once for every matching waiter. That turns a single incoming stanza intoO(matches)deep clones right in the resolve path, which undercuts the zero-copy work when multiple waiters observe the same node.♻️ Proposed fix
fn resolve_waiters( waiters_mutex: &std::sync::Mutex<Vec<NodeWaiter>>, counter: &AtomicUsize, node: &Arc<wacore_binary::OwnedNodeRef>, ) { let nr = node.get(); + let mut shared_owned: Option<Arc<Node>> = None; let mut waiters = waiters_mutex .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); let mut i = 0; while i < waiters.len() { @@ } else if waiters[i].filter.matches(nr) { let w = waiters.swap_remove(i); counter.fetch_sub(1, Ordering::Release); - let _ = w.tx.send(Arc::new(node.to_owned_node())); + let owned = shared_owned + .get_or_insert_with(|| Arc::new(node.to_owned_node())) + .clone(); + let _ = w.tx.send(owned); } else { i += 1; } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 96 - 114, In resolve_waiters: avoid calling node.to_owned_node() for every matching waiter; create a single Arc-owned payload (e.g., let owned = Arc::new(node.to_owned_node())) the first time you hit a matching waiter and then send Arc::clone(&owned) to each subsequent waiter (use Arc::clone or owned.clone()) instead of Arc::new(node.to_owned_node()); this keeps the existing swap_remove/counter.fetch_sub logic but replaces per-waiter deep clones with cheap Arc clones to broadcast the same owned payload.
♻️ Duplicate comments (1)
src/handlers/ib.rs (1)
29-37:⚠️ Potential issue | 🟠 MajorAvoid re-materializing
Nodein the handler hot path.Line 35 (
to_owned_node()) reintroduces a full clone/allocation per<ib>stanza and undermines the zero-copy migration.♻️ Proposed zero-copy adjustment
-use wacore_binary::node::{Node, NodeContent}; +use wacore_binary::node::NodeContent; impl StanzaHandler for IbHandler { @@ async fn handle( &self, client: Arc<Client>, node: Arc<wacore_binary::OwnedNodeRef>, _cancelled: &mut bool, ) -> bool { - let owned = node.to_owned_node(); - handle_ib_impl(client, &owned).await; + handle_ib_impl(client, node.as_ref()).await; true } } -async fn handle_ib_impl(client: Arc<Client>, node: &Node) { - for child in node.children().unwrap_or_default() { +async fn handle_ib_impl(client: Arc<Client>, node: &wacore_binary::OwnedNodeRef) { + let node = node.get(); + for child in node.children().unwrap_or_default() { match child.tag.as_ref() { // unchanged🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 29 - 37, The handler currently clones the Node with node.to_owned_node(), which allocates per stanza; instead change handle_ib_impl's signature to take a borrowed OwnedNodeRef (e.g., &wacore_binary::OwnedNodeRef) and call it with a reference to the existing Arc-held node (for example handle_ib_impl(client, &*node).await or handle_ib_impl(client, node.as_ref()).await) so no full Node is re-materialized; update the handle_ib_impl function name/signature and all its call sites accordingly (replace uses expecting owned Node with borrowed accessors).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client.rs`:
- Around line 3182-3184: The code currently clones every incoming Node via
node.to_owned() before calling pair::handle_iq, which is expensive on the hot IQ
path; either change pair::handle_iq to accept a non-owning NodeRef/OwnedNodeRef
(e.g., update pair::handle_iq signature to take &NodeRef or OwnedNodeRef and
adjust pair.rs to work with that) so no clone is required for non-pair stanzas,
or add a cheap pre-check (inspect NodeRef metadata such as node.tag/name,
namespace, or presence of a known pair-specific child/attribute) before calling
node.to_owned() and only materialize the owned Node when the pre-check indicates
the stanza might be for pairing. Ensure you update calls and tests that invoke
pair::handle_iq to match the new signature if you choose the first option.
- Around line 4885-4892: The test helper node_to_owned_ref currently strips the
leading format byte by slicing bytes[1..], which hardcodes framing; update it to
use the same unpack logic as production by calling wacore_binary::util::unpack
(or the equivalent helper used by decrypt_frame) on the marshalled bytes and
pass the returned payload into OwnedNodeRef::new so tests mirror production
unpack/compression handling; locate node_to_owned_ref and replace the bytes[1..]
slicing with the unpack call and use its output for OwnedNodeRef::new.
In `@src/handlers/basic.rs`:
- Around line 84-91: The current handler calls OwnedNodeRef::to_owned_node(),
which clones the entire node and defeats the allocation savings—change
Client::handle_ack_response to accept a borrowed &wacore_binary::NodeRef<'_> and
call client.handle_ack_response(node.get()). Specifically, update the signature
of Client::handle_ack_response to take &wacore_binary::NodeRef<'_> and move the
ack-parsing logic into that borrowed form, then replace the to_owned_node() call
in handle with passing node.get() so no full clone is performed.
In `@src/handlers/receipt.rs`:
- Around line 22-30: The current call creates a new Owned Node and Arc every
receipt: Arc::new(node.to_owned_node()) inside handle should be replaced by
updating Client::handle_receipt's signature to avoid that allocation; change
Client::handle_receipt to accept either a reference (&Node or &OwnedNodeRef) if
it only needs read access, then call it with node.as_ref() or &*node (no
allocation), or change it to accept Arc<OwnedNodeRef> and call it with
Arc::clone(&node) to reuse the existing Arc; update the Client::handle_receipt
definition and all callers (and their imports) accordingly to match the new
parameter type.
In `@wacore/binary/src/jid.rs`:
- Around line 627-631: The branch that returns Err(JidError::InvalidFormat(...))
is constructing an error string that already includes the "Invalid JID format:"
prefix, causing duplication because JidError's Display impl adds that prefix;
change the returned InvalidFormat payload to just the server identifier or a
message without the "Invalid JID format:" prefix (e.g., server.to_string() or a
concise explanation like "unknown server '{server}'") so that
JidError::InvalidFormat and its Display produce a single prefixed message;
locate the return in the conditional around user_part.is_empty() and
Server::try_from(server).
- Around line 385-390: Call sites passing a &str to Jid::new must be updated to
supply a Server instance instead; find all uses of Jid::new(..., "...") and
replace the string literal with a Server constructed from that string (e.g.
Server::from("example.com"), Server::new("example.com"), or
Server::try_from("example.com").unwrap(), depending on which constructor/trait
the Server type exposes), so that every Jid::new invocation provides a Server
rather than a &str.
---
Outside diff comments:
In `@src/client.rs`:
- Around line 96-114: In resolve_waiters: avoid calling node.to_owned_node() for
every matching waiter; create a single Arc-owned payload (e.g., let owned =
Arc::new(node.to_owned_node())) the first time you hit a matching waiter and
then send Arc::clone(&owned) to each subsequent waiter (use Arc::clone or
owned.clone()) instead of Arc::new(node.to_owned_node()); this keeps the
existing swap_remove/counter.fetch_sub logic but replaces per-waiter deep clones
with cheap Arc clones to broadcast the same owned payload.
In `@wacore/binary/src/decoder.rs`:
- Around line 109-184: Tests are missing for the new server-decoding branches;
add table-driven unit tests that exercise malformed and valid-server paths for
read_jid_pair, read_ad_jid, read_interop_jid, and read_fb_jid. For each function
create inputs that produce: successful JidRef (when server string matches
expected), InvalidNode (when missing/empty user or wrong interop/messenger
server), and AttrParse (for read_jid_pair when server string fails
Server::try_from), assert the returned Result matches BinaryError::InvalidNode
or BinaryError::AttrParse or the expected JidRef fields; reference the decoder
methods (read_jid_pair, read_ad_jid, read_interop_jid, read_fb_jid) and
constants crate::jid::INTEROP_SERVER and crate::jid::MESSENGER_SERVER to build
the test cases so regressions in server-decoding are caught.
---
Duplicate comments:
In `@src/handlers/ib.rs`:
- Around line 29-37: The handler currently clones the Node with
node.to_owned_node(), which allocates per stanza; instead change
handle_ib_impl's signature to take a borrowed OwnedNodeRef (e.g.,
&wacore_binary::OwnedNodeRef) and call it with a reference to the existing
Arc-held node (for example handle_ib_impl(client, &*node).await or
handle_ib_impl(client, node.as_ref()).await) so no full Node is re-materialized;
update the handle_ib_impl function name/signature and all its call sites
accordingly (replace uses expecting owned Node with borrowed accessors).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 69d87b85-6556-4c53-80cc-a61747216feb
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (25)
Cargo.tomlsrc/client.rssrc/client/device_registry.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/unified_session.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/src/iq/privacy.rs
Benchmark Results3 improvement(s):
56 unchanged benchmark(s)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab548eabdc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 128 => crate::jid::Server::Hosted, | ||
| 129 => crate::jid::Server::HostedLid, | ||
| n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, | ||
| _ => crate::jid::Server::Pn, |
There was a problem hiding this comment.
Reject unsupported AD_JID domain types during decode
The new read_ad_jid mapping now coerces every unrecognized domain type to Server::Pn instead of returning an error. This silently reinterprets malformed or future domain bytes as PN identities, so stanzas can be routed through the wrong identity/session paths rather than being rejected as invalid input (the previous behavior).
Useful? React with 👍 / 👎.
| node.get_attr(k.as_str()) | ||
| .and_then(|attr| attr.as_str()) | ||
| .is_some_and(|s| s == v.as_str()) |
There was a problem hiding this comment.
Match JID attributes in node waiter filters
NodeFilter::matches now only compares attributes via attr.as_str(), but decoded JID attributes are represented as ValueRef::Jid (where as_str() is None). That means filters like NodeFilter::from_jid(...) (or any attr match against a JID-valued field) will never match incoming decoded nodes, so wait_for_node can time out even when the stanza arrived.
Useful? React with 👍 / 👎.
f0b4c50 to
cedf071
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
src/client.rs (2)
4885-4892: 🧹 Nitpick | 🔵 TrivialMirror production unpacking in this helper.
Slicing
bytes[1..]hardcodes the current framing byte and skips the sameutil::unpackpath used bydecrypt_frame, so these tests can drift from real behavior.🧪 Suggested fix
/// Helper to create an `Arc<OwnedNodeRef>` from a `Node` for tests. fn node_to_owned_ref(node: Node) -> Arc<wacore_binary::OwnedNodeRef> { - let bytes = wacore_binary::marshal::marshal(&node).expect("marshal should succeed"); - // marshal() prepends a leading 0x00 format byte; OwnedNodeRef::new expects raw protocol bytes without it + let bytes = wacore_binary::marshal::marshal_auto(&node).expect("marshal should succeed"); + let unpacked = wacore_binary::util::unpack(&bytes) + .expect("unpack should succeed") + .into_owned(); Arc::new( - wacore_binary::OwnedNodeRef::new(bytes[1..].to_vec()) + wacore_binary::OwnedNodeRef::new(unpacked) .expect("OwnedNodeRef::new should succeed"), ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4885 - 4892, The test helper node_to_owned_ref currently slices off the first byte (bytes[1..]) which hardcodes the framing and diverges from production unpacking; change it to call the same unpacking used in decrypt_frame (e.g., util::unpack or the project’s unpack function) on the marshalled bytes returned by wacore_binary::marshal::marshal and then pass the unpacked/raw protocol bytes into wacore_binary::OwnedNodeRef::new so tests mirror production framing behavior (keep the existing expect messages).
3182-3184:⚠️ Potential issue | 🟠 MajorAvoid cloning every non-pair IQ before dispatch.
node.to_owned()runs before we know the stanza is pairing-related, so unrelated IQs still pay a full deep copy on the common path. Add a cheap pair-specific guard before materializing an ownedNode, or movepair::handle_iqontoNodeRef/OwnedNodeRef.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3182 - 3184, The code currently clones every incoming Node via node.to_owned() before calling pair::handle_iq, causing unnecessary deep copies for non-pair IQs; fix this by either (A) adding a cheap, non-allocating guard that inspects the NodeRef (e.g. check stanza type, tag/child name or an attribute that identifies pairing IQs) and only call node.to_owned() when that guard indicates a pairing IQ, or (B) update pair::handle_iq to accept a NodeRef/OwnedNodeRef (e.g. &NodeRef) so you can call pair::handle_iq(self, &node).await without cloning; locate the use of node.to_owned() and pair::handle_iq in the client.rs snippet and apply one of these changes to avoid the unnecessary deep copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client.rs`:
- Around line 1655-1662: The IQ response path is forcing a deep copy via
node.to_owned_node() before sending to waiters; change the waiter payload type
and send path so we forward a zero-copy OwnedNodeRef (or Arc<OwnedNodeRef>)
instead of an OwnedNode, avoiding to_owned_node(). Update the response_waiters
map entry type and any waiter.send calls (the waiter receiver and its creation
sites) to accept OwnedNodeRef or Arc<OwnedNodeRef>, and when dispatching for IQ
(where nr.tag == "iq" and id is present) send the existing node reference (or
wrap it in Arc) rather than calling node.to_owned_node(); ensure constructors of
waiters use the new type so decode remains zero-copy through request/response
handling.
- Around line 81-87: The matches method currently only checks attr.as_str(),
which misses JID-backed attributes; update the attribute comparison inside
matches (the closure using node.get_attr(...).and_then(|attr| attr.as_str())...)
to accept JID-backed values too by extracting the attribute as either a plain
string or a JID and comparing their string forms (e.g., try attr.as_str() first,
then attr.as_jid().map(|j| j.to_string()) or equivalent) against the filter
value; keep the outer logic and use the same attribute key lookup on NodeRef so
NodeFilter::from_jid and sent-node waiters will match JID-backed attrs.
In `@src/handlers/chatstate.rs`:
- Around line 56-63: The current fix resolves the type mismatch by calling
node.to_owned_node() before ChatstateStanza::parse, but that allocates and loses
zero-copy benefits; change ChatstateStanza::parse to take a borrowed node
reference (e.g., &wacore_binary::NodeRef<'_> or
&wacore_binary::OwnedNodeRef::NodeRef view) so you can pass a reference directly
from the Arc<wacore_binary::OwnedNodeRef> without calling to_owned_node(),
update the parse signature and all callers (including handle) to accept the
borrowed reference, and remove the to_owned_node() allocation where possible to
preserve zero-copy semantics.
In `@src/handlers/notification.rs`:
- Around line 37-45: The handler currently calls node.to_owned_node(), which
deep-clones the stanza and defeats zero-copy; instead, stop rematerializing and
call handle_notification_impl with a reference to the existing node (e.g., pass
&*node or node.as_ref()) and change handle_notification_impl (and any helpers it
calls) to accept borrowed node types (prefer wacore_binary::NodeRef or
&wacore_binary::OwnedNodeRef) rather than an owned cloned node; update all call
sites of handle_notification_impl and its helpers to the new signature so they
operate on NodeRef/OwnedNodeRef references and preserve the zero-copy decode
path.
In `@src/message.rs`:
- Around line 3585-3586: The test code currently masks failures by using
unwrap_or(Server::Lid) after wacore_binary::jid::Server::try_from(lid_server);
replace each unwrap_or(...) with expect(...) on the Result returned by try_from
so a mapping failure is explicit (e.g., change
wacore_binary::jid::Server::try_from(lid_server).unwrap_or(wacore_binary::jid::Server::Lid)
to wacore_binary::jid::Server::try_from(lid_server).expect("unexpected Server
mapping for lid_server") ), and make the same replacement for the other
identical occurrences of try_from(...).unwrap_or(...) in this file so tests fail
visibly on bad mappings.
In `@wacore/benches/send_receive_benchmark.rs`:
- Around line 190-193: The benchmark fixture silently falls back on Server::Pn
due to unwrap_or_default; change the call in the Jid::new construction to fail
fast by replacing unwrap_or_default() with expect(...) so invalid server strings
cause an immediate panic; specifically update the
wacore_binary::jid::Server::try_from(server) usage inside the Jid::new call to
use expect with a clear message (e.g., "invalid server in benchmark fixture") so
Jid::new and Server::try_from failures surface during setup.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 4885-4892: The test helper node_to_owned_ref currently slices off
the first byte (bytes[1..]) which hardcodes the framing and diverges from
production unpacking; change it to call the same unpacking used in decrypt_frame
(e.g., util::unpack or the project’s unpack function) on the marshalled bytes
returned by wacore_binary::marshal::marshal and then pass the unpacked/raw
protocol bytes into wacore_binary::OwnedNodeRef::new so tests mirror production
framing behavior (keep the existing expect messages).
- Around line 3182-3184: The code currently clones every incoming Node via
node.to_owned() before calling pair::handle_iq, causing unnecessary deep copies
for non-pair IQs; fix this by either (A) adding a cheap, non-allocating guard
that inspects the NodeRef (e.g. check stanza type, tag/child name or an
attribute that identifies pairing IQs) and only call node.to_owned() when that
guard indicates a pairing IQ, or (B) update pair::handle_iq to accept a
NodeRef/OwnedNodeRef (e.g. &NodeRef) so you can call pair::handle_iq(self,
&node).await without cloning; locate the use of node.to_owned() and
pair::handle_iq in the client.rs snippet and apply one of these changes to avoid
the unnecessary deep copy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9ad351bd-03de-4a55-b330-43d597272468
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (26)
Cargo.tomlsrc/client.rssrc/client/device_registry.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/unified_session.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/src/iq/privacy.rs
d2fcf22 to
951c6d5
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (8)
src/message.rs (1)
3585-3586:⚠️ Potential issue | 🟡 MinorDon't silently recover from a broken server mapping in these tests.
unwrap_or(Server::Lid)keeps these branches green even iflid_serverstops mapping correctly, which weakens the regression signal for the enum migration. Useexpect(...)so a bad mapping fails loudly.Minimal fix
- server: wacore_binary::jid::Server::try_from(lid_server) - .unwrap_or(wacore_binary::jid::Server::Lid), + server: wacore_binary::jid::Server::try_from(lid_server) + .expect("lid_server should map to Server::Lid"),Also applies to: 3595-3596, 3704-3705, 3714-3715, 3809-3810, 3818-3819
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 3585 - 3586, Replace the silent fallback from unwrap_or(Server::Lid) with expect so tests fail loudly when the mapping is broken: locate each use of wacore_binary::jid::Server::try_from(lid_server).unwrap_or(wacore_binary::jid::Server::Lid) (e.g., in src/message.rs at the instances around the try_from calls you've changed) and change to call expect(...) on the Result, providing a clear message like "unexpected Server mapping for lid_server: {lid_server}" so a bad mapping panics instead of silently using Server::Lid; apply the same replacement to the other occurrences noted (around the other try_from sites).src/handlers/notification.rs (1)
37-44: 🛠️ Refactor suggestion | 🟠 MajorThis handler still breaks the zero-copy path.
node.to_owned_node()rematerializes the full stanza before any parsing, so notifications lose the allocation savings introduced byOwnedNodeRef. Pushhandle_notification_impland its helpers ontoOwnedNodeRef/NodeRefinstead of cloning at the boundary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/notification.rs` around lines 37 - 44, The handler currently calls node.to_owned_node(), which defeats zero-copy by rematerializing the stanza; instead, change async fn handle to pass a reference to the existing OwnedNodeRef/NodeRef (e.g., &Arc<wacore_binary::OwnedNodeRef> or &wacore_binary::NodeRef) into the processing path and move handle_notification_impl and its helper functions as methods or free functions that accept &OwnedNodeRef / &NodeRef (or implement them as impl blocks on OwnedNodeRef/NodeRef). Replace the to_owned_node() call in handle with a direct call into the new OwnedNodeRef/NodeRef-based method (e.g., node.handle_notification_impl(...)) and update all helper usages to operate on the reference types so no cloning/rematerialization occurs at the boundary.src/handlers/chatstate.rs (1)
56-63: 🧹 Nitpick | 🔵 Trivial
ChatstateStanza::parseis still forcing an owned clone.This handler immediately materializes the stanza before parsing, so the new
OwnedNodeRefAPI does not reduce allocations on this path yet. If you switch the parser to&NodeRef<'_>, chatstate handling stays zero-copy end to end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/chatstate.rs` around lines 56 - 63, The handler calls node.to_owned_node() and then ChatstateStanza::parse(&owned), forcing an owned clone; update the parser to accept a borrowed node reference (e.g., change ChatstateStanza::parse signature to take &NodeRef<'_>) and call it with the existing node reference instead of creating an OwnedNodeRef (remove the to_owned_node() usage in the handle method). Ensure ChatstateStanza::parse and any downstream uses are updated to work with &NodeRef<'_> so chatstate handling remains zero-copy end-to-end.src/handlers/receipt.rs (1)
22-29: 🛠️ Refactor suggestion | 🟠 MajorStop rebuilding a
Nodeon every receipt.
Arc::new(node.to_owned_node())brings back a full tree copy plus a newArcallocation on one of the busiest stanza paths. To preserve the zero-copy win here, moveClient::handle_receiptto&NodeRef<'_>orArc<OwnedNodeRef>and pass the borrowed/ref-counted node through directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/receipt.rs` around lines 22 - 29, The handler is cloning the entire node tree with node.to_owned_node() on every receipt; change Client::handle_receipt to accept a ref-counted node (Arc<wacore_binary::OwnedNodeRef>) or a borrowed NodeRef (e.g., &wacore_binary::NodeRef) so you can pass the node directly without cloning; update the Client::handle_receipt signature and all its callers/implementations accordingly, and in receipt.rs call client.handle_receipt(node.clone()).await (or pass &*node if you choose &NodeRef) instead of Arc::new(node.to_owned_node()) to preserve zero-copy and avoid the tree copy.src/handlers/basic.rs (1)
84-93: 🧹 Nitpick | 🔵 Trivial
<ack>handler still allocates viato_owned_node().This was flagged in a previous review: calling
to_owned_node()clones the entire node, negating the zero-copy benefit for ack handling. The suggested fix was to changeClient::handle_ack_responseto accept&wacore_binary::NodeRef<'_>and passnode.get()directly.Consider refactoring
handle_ack_responseto accept a borrowed reference:♻️ Suggested direction
- let owned_node = node.to_owned_node(); - client.handle_ack_response(owned_node).await; + client.handle_ack_response(node.get()).await;And update
handle_ack_responsesignature insrc/client.rs:pub(crate) async fn handle_ack_response( self: &Arc<Self>, node: &wacore_binary::NodeRef<'_>, ) { // existing ack parsing logic adapted for NodeRef }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/basic.rs` around lines 84 - 93, The handler currently clones the node via to_owned_node() and passes an OwnedNodeRef into Client::handle_ack_response, losing zero-copy benefits; change the handler in handle(...) to call client.handle_ack_response with a borrowed NodeRef by passing node.get() (i.e., a &wacore_binary::NodeRef<'_>) instead of owned_node, and update Client::handle_ack_response signature (and its callers) to accept self: &Arc<Self> and node: &wacore_binary::NodeRef<'_> so the ack parsing logic works with a borrowed NodeRef without cloning.src/client.rs (3)
4886-4893:⚠️ Potential issue | 🟡 MinorMake the test helper use the same unpack contract as production.
Hardcoding
bytes[1..]bakes in the current framing assumption, whiledecrypt_framegoes throughwacore_binary::util::unpack. If framing or compression changes again, these tests can drift from the real receive path.🧪 Proposed fix
fn node_to_owned_ref(node: Node) -> Arc<wacore_binary::OwnedNodeRef> { - let bytes = wacore_binary::marshal::marshal(&node).expect("marshal should succeed"); - // marshal() prepends a leading 0x00 format byte; OwnedNodeRef::new expects raw protocol bytes without it + let bytes = wacore_binary::marshal::marshal_auto(&node).expect("marshal should succeed"); + let unpacked = wacore_binary::util::unpack(&bytes) + .expect("unpack should succeed") + .into_owned(); Arc::new( - wacore_binary::OwnedNodeRef::new(bytes[1..].to_vec()) + wacore_binary::OwnedNodeRef::new(unpacked) .expect("OwnedNodeRef::new should succeed"), ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4886 - 4893, The test helper node_to_owned_ref currently strips the leading format byte by slicing bytes[1..] which diverges from production; instead call wacore_binary::util::unpack on the marshaled bytes (the same contract decrypt_frame uses) and pass the unpacked payload into wacore_binary::OwnedNodeRef::new, propagating or expect()-ing the Result as appropriate so tests exercise the same framing/unpacking logic as production.
1656-1663:⚠️ Potential issue | 🟠 MajorKeep IQ waiter delivery zero-copy.
node.to_owned_node()rematerializes the full stanza on every matched IQ response, which puts allocation/copy back onto a very common path (connect, props, app-state sync, etc.). This optimization currently stops at the waiter boundary; carryArc<OwnedNodeRef>throughresponse_waitersand only materialize an ownedNodeat the final consumer that truly needs it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1656 - 1663, The code currently calls node.to_owned_node() when delivering IQ responses which forces allocation; change the response_waiters storage and send path to carry Arc<OwnedNodeRef> instead of Node so delivery is zero-copy: update the type stored in self.response_waiters to Arc<OwnedNodeRef>, stop calling node.to_owned_node() here, send an Arc clone (e.g. waiter.send(arc_node.clone())), and defer calling to_owned_node() only in the final consumer that actually needs an owned Node; adjust any related type signatures (response_waiters map value, waiter channel type, and consumer handlers) to accept Arc<OwnedNodeRef>.
3183-3185:⚠️ Potential issue | 🟠 MajorDon’t deep-clone every non-ping IQ before pair dispatch.
The unconditional
node.to_owned()reintroduces a full copy on the hot IQ path even when the stanza is unrelated to pairing. Either makepair::handle_iqconsume a ref-based node, or gate the materialization behind a cheap pair-specific pre-check.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3183 - 3185, The code clones every incoming IQ via node.to_owned() before dispatching to pair::handle_iq, causing unnecessary copies; either change pair::handle_iq signature to accept a borrow (e.g., &Node or &Stanza) so you can pass &node directly, or add a cheap pre-check (inspect node.tag()/attrs/type/ping marker) and only call node.to_owned() when the stanza is actually pair-related; update all call sites and types in pair::handle_iq (and its helpers) accordingly to avoid the unconditional deep clone.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client.rs`:
- Around line 3624-3637: build_ack_node currently calls to_string_cow() when
constructing NodeValue for id, from, participant and type which forces
JID-backed attributes through stringification and extra allocations; instead,
preserve the typed/borrowed attribute values when creating NodeValue (avoid
to_string_cow()) by using the attribute value directly from node.get_attr(...)
or the NodeRef API that returns a borrowed/typed representation so NodeValue is
built without intermediate String allocation (update the constructions for id,
from, participant and the type branch in build_ack_node to use the
direct/borrowed attr values).
In `@src/handlers/ib.rs`:
- Around line 29-36: The code currently calls node.to_owned_node() in handle
which unnecessarily allocates the full tree; change handle to pass a borrowed
reference instead by using node.get() or &NodeRef<'_> and update handle_ib_impl
signature to accept a borrowed NodeRef (or &NodeRef) so it reads from the
borrowed data without allocation; ensure call sites using handle_ib_impl (and
its async signature) are adjusted to accept the borrowed type and that lifetimes
compile.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 4886-4893: The test helper node_to_owned_ref currently strips the
leading format byte by slicing bytes[1..] which diverges from production;
instead call wacore_binary::util::unpack on the marshaled bytes (the same
contract decrypt_frame uses) and pass the unpacked payload into
wacore_binary::OwnedNodeRef::new, propagating or expect()-ing the Result as
appropriate so tests exercise the same framing/unpacking logic as production.
- Around line 1656-1663: The code currently calls node.to_owned_node() when
delivering IQ responses which forces allocation; change the response_waiters
storage and send path to carry Arc<OwnedNodeRef> instead of Node so delivery is
zero-copy: update the type stored in self.response_waiters to Arc<OwnedNodeRef>,
stop calling node.to_owned_node() here, send an Arc clone (e.g.
waiter.send(arc_node.clone())), and defer calling to_owned_node() only in the
final consumer that actually needs an owned Node; adjust any related type
signatures (response_waiters map value, waiter channel type, and consumer
handlers) to accept Arc<OwnedNodeRef>.
- Around line 3183-3185: The code clones every incoming IQ via node.to_owned()
before dispatching to pair::handle_iq, causing unnecessary copies; either change
pair::handle_iq signature to accept a borrow (e.g., &Node or &Stanza) so you can
pass &node directly, or add a cheap pre-check (inspect
node.tag()/attrs/type/ping marker) and only call node.to_owned() when the stanza
is actually pair-related; update all call sites and types in pair::handle_iq
(and its helpers) accordingly to avoid the unconditional deep clone.
In `@src/handlers/basic.rs`:
- Around line 84-93: The handler currently clones the node via to_owned_node()
and passes an OwnedNodeRef into Client::handle_ack_response, losing zero-copy
benefits; change the handler in handle(...) to call client.handle_ack_response
with a borrowed NodeRef by passing node.get() (i.e., a
&wacore_binary::NodeRef<'_>) instead of owned_node, and update
Client::handle_ack_response signature (and its callers) to accept self:
&Arc<Self> and node: &wacore_binary::NodeRef<'_> so the ack parsing logic works
with a borrowed NodeRef without cloning.
In `@src/handlers/chatstate.rs`:
- Around line 56-63: The handler calls node.to_owned_node() and then
ChatstateStanza::parse(&owned), forcing an owned clone; update the parser to
accept a borrowed node reference (e.g., change ChatstateStanza::parse signature
to take &NodeRef<'_>) and call it with the existing node reference instead of
creating an OwnedNodeRef (remove the to_owned_node() usage in the handle
method). Ensure ChatstateStanza::parse and any downstream uses are updated to
work with &NodeRef<'_> so chatstate handling remains zero-copy end-to-end.
In `@src/handlers/notification.rs`:
- Around line 37-44: The handler currently calls node.to_owned_node(), which
defeats zero-copy by rematerializing the stanza; instead, change async fn handle
to pass a reference to the existing OwnedNodeRef/NodeRef (e.g.,
&Arc<wacore_binary::OwnedNodeRef> or &wacore_binary::NodeRef) into the
processing path and move handle_notification_impl and its helper functions as
methods or free functions that accept &OwnedNodeRef / &NodeRef (or implement
them as impl blocks on OwnedNodeRef/NodeRef). Replace the to_owned_node() call
in handle with a direct call into the new OwnedNodeRef/NodeRef-based method
(e.g., node.handle_notification_impl(...)) and update all helper usages to
operate on the reference types so no cloning/rematerialization occurs at the
boundary.
In `@src/handlers/receipt.rs`:
- Around line 22-29: The handler is cloning the entire node tree with
node.to_owned_node() on every receipt; change Client::handle_receipt to accept a
ref-counted node (Arc<wacore_binary::OwnedNodeRef>) or a borrowed NodeRef (e.g.,
&wacore_binary::NodeRef) so you can pass the node directly without cloning;
update the Client::handle_receipt signature and all its callers/implementations
accordingly, and in receipt.rs call client.handle_receipt(node.clone()).await
(or pass &*node if you choose &NodeRef) instead of
Arc::new(node.to_owned_node()) to preserve zero-copy and avoid the tree copy.
In `@src/message.rs`:
- Around line 3585-3586: Replace the silent fallback from unwrap_or(Server::Lid)
with expect so tests fail loudly when the mapping is broken: locate each use of
wacore_binary::jid::Server::try_from(lid_server).unwrap_or(wacore_binary::jid::Server::Lid)
(e.g., in src/message.rs at the instances around the try_from calls you've
changed) and change to call expect(...) on the Result, providing a clear message
like "unexpected Server mapping for lid_server: {lid_server}" so a bad mapping
panics instead of silently using Server::Lid; apply the same replacement to the
other occurrences noted (around the other try_from sites).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: dd9a7152-8a87-4043-95da-c5f24d49bfff
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (27)
Cargo.tomlsrc/client.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/unified_session.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/src/iq/privacy.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 951c6d53c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | ||
| #[repr(u8)] | ||
| pub enum Server { |
There was a problem hiding this comment.
Preserve wire-format server strings in serde output
Deriving Serialize/Deserialize directly on Server changes JID JSON shape from protocol domains (for example "s.whatsapp.net") to enum variant names (for example "Pn"). Because wacore enables wacore-binary's serde feature, this affects all serialized Jid fields in public events/state and breaks backward compatibility when reading previously stored JSON that still contains domain strings. Add explicit serde renames (or custom serde impls) so Server encodes/decodes using as_str() values.
Useful? React with 👍 / 👎.
951c6d5 to
d4f7e86
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4f7e86277
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let from = NodeValue::from(node.get_attr("from")?.to_string_cow().as_ref()); | ||
| let participant = node | ||
| .get_attr("participant") | ||
| .map(|v| NodeValue::from(v.to_string_cow().as_ref())); |
There was a problem hiding this comment.
Preserve JID structure in ACK destination attrs
Avoid converting from/participant to plain strings when building ACKs, because this drops structured JID fields that are not representable in Display output (notably integrator for INTEROP_JID). In this path, an incoming interop stanza can be ACKed with to rewritten as a plain JID_PAIR (or otherwise normalized) instead of the original structured JID, which can cause ACK routing/matching failures for interop traffic.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/client/lid_pn.rs (1)
139-150:⚠️ Potential issue | 🟡 MinorUse typed
Serverenum variants instead of string constants for consistency.Lines 142 and 145 compare
target.serveragainst string constants (DEFAULT_USER_SERVER,HIDDEN_USER_SERVER), while line 150 constructs with the typedServer::Lidenum. AlthoughServerimplementsPartialEq<str>and the code works, mixing string and typed comparisons reduces consistency. Replace the string-based branching with typedServer::PnandServer::Lidvariants.Suggested change
- let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER; - let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER; - - if target.server == lid_server { + if target.server == Server::Lid { // Already a LID - use it directly target.clone() - } else if target.server == pn_server { + } else if target.server == Server::Pn { // PN JID - check if we have a LID mapping🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/lid_pn.rs` around lines 139 - 150, The code compares target.server to string constants DEFAULT_USER_SERVER and HIDDEN_USER_SERVER; instead, use the typed Server enum variants for consistency—replace checks against wacore_binary::jid::DEFAULT_USER_SERVER and wacore_binary::jid::HIDDEN_USER_SERVER with comparisons to wacore_binary::jid::Server::Pn and wacore_binary::jid::Server::Lid respectively (keeping the existing Jid construction that uses Server::Lid), and adjust any imports or fully-qualify Server where needed so all server equality checks consistently use the Server enum.src/handlers/ib.rs (1)
128-133:⚠️ Potential issue | 🟠 MajorAvoid direct
Devicemutation in the edge-routing write path.This code writes device state via
modify_device(...)directly. Route this throughDeviceCommand+PersistenceManager::process_command()to keep state transitions centralized and consistent.As per coding guidelines: Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 128 - 133, The direct mutation using client_clone.persistence_manager.modify_device(...) to set device.edge_routing_info = Some(routing_bytes) must be replaced by constructing an appropriate DeviceCommand (e.g., SetEdgeRouting or similar) carrying routing_bytes and sending it to client_clone.persistence_manager.process_command(command). Also update reads to use client_clone.persistence_manager.get_device_snapshot() where needed; ensure you remove the direct modify_device call and use the PersistenceManager::process_command() path so state changes go through centralized command handling.src/message.rs (2)
73-90:⚠️ Potential issue | 🟡 MinorWarn before dropping malformed newsletter plaintext.
If
<plaintext>exists but its content is not bytes, this path now returns silently and the stanza disappears without any diagnostic. Please log that case before returning.💡 Minimal fix
- if let Some(NodeContentRef::Bytes(bytes)) = plaintext_node.content.as_deref() { - match wa::Message::decode(bytes.as_ref()) { - Ok(msg) => { - log::info!( - "[msg:{}] Received newsletter plaintext message from {}", - info.id, - info.source.chat - ); - self.dispatch_parsed_message(msg, info); - } - Err(e) => { - log::warn!( - "[msg:{}] Failed to decode newsletter plaintext: {e}", - info.id - ); - } - } - } + let Some(NodeContentRef::Bytes(bytes)) = plaintext_node.content.as_deref() else { + log::warn!( + "[msg:{}] Newsletter <plaintext> child has non-byte content", + info.id + ); + return; + }; + + match wa::Message::decode(bytes.as_ref()) { + Ok(msg) => { + log::info!( + "[msg:{}] Received newsletter plaintext message from {}", + info.id, + info.source.chat + ); + self.dispatch_parsed_message(msg, info); + } + Err(e) => { + log::warn!( + "[msg:{}] Failed to decode newsletter plaintext: {e}", + info.id + ); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 73 - 90, The code silently returns when plaintext_node.content exists but isn't NodeContentRef::Bytes; update the handling in the plaintext block surrounding plaintext_node and wa::Message::decode so that if plaintext_node.content.as_deref() is Some but not Bytes you emit a warning (e.g., using log::warn!) that includes info.id and a brief description that plaintext content was present but not bytes before returning, while keeping the existing decode/dispatch_parsed_message flow for the Bytes case.
366-372: 🧹 Nitpick | 🔵 TrivialAvoid re-acquiring
custom_enc_handlersfor every<enc>node.This
read().awaitsits inside the hot per-node decode loop. Hoisting the read guard to the loop scope removes repeated async lock acquisitions for multi-enc stanzas; just keep that scope tight so the guard is dropped before the next.awaitafter the loop.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 366 - 372, The code repeatedly calls self.custom_enc_handlers.read().await inside the per-<enc> node decode loop which reacquires the async RwLock on every node; hoist the read guard outside the inner node loop by calling let guard = self.custom_enc_handlers.read().await once (or at the start of the multi-enc stanza), use guard.get(enc_type.as_ref()).cloned() to fetch the handler, then drop the guard (or let it go out of scope) before any subsequent await in the loop so the async lock isn't held across awaits and isn't re-acquired per node; refer to custom_enc_handlers, enc_type, and the handler variable when making this change.
♻️ Duplicate comments (6)
src/handlers/notification.rs (1)
206-211: 🧹 Nitpick | 🔵 Trivial
to_owned_node()conversions remain for unmigrated APIs.Several notification handlers still call
to_owned_node()before passing to parsing functions (e.g.,handle_pair_code_notification,DeviceNotification::try_parse,BusinessNotification::try_parse,GroupNotification::try_from_node). This defeats zero-copy for these paths but is necessary until downstream APIs are migrated to accept&NodeRef<'_>.Consider tracking migration of these parsing APIs as follow-up work to fully realize the zero-copy benefits.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/notification.rs` around lines 206 - 211, Several handlers currently call to_owned_node() which forces allocations and prevents zero-copy; update the parsing/handler APIs to accept a NodeRef<'_> (borrowed) and then pass &node (the existing NodeRef) instead of calling to_owned_node() in call sites such as crate::pair_code::handle_pair_code_notification, DeviceNotification::try_parse, BusinessNotification::try_parse, and GroupNotification::try_from_node; change those function signatures to take &NodeRef<'_> (or &impl AsNodeRef) and update their internals to work with the borrowed node so you can remove the to_owned_node() conversions across these notification handlers, tracking any remaining unported call sites as follow-up work.src/message.rs (1)
3598-3599:⚠️ Potential issue | 🟡 MinorFail these server-mapping tests loudly instead of defaulting to
Server::Lid.These
unwrap_or(...)fallbacks still hide a brokenlid_server→Servermapping and keep the migration tests green. In test code, preferexpect(...)so the regression is visible.🔎 Minimal fix
- server: wacore_binary::jid::Server::try_from(lid_server) - .unwrap_or(wacore_binary::jid::Server::Lid), + server: wacore_binary::jid::Server::try_from(lid_server) + .expect("lid_server should map to a typed Server"),Apply the same replacement to the other identical copies in this file.
Also applies to: 3608-3609, 3717-3718, 3727-3728, 3822-3823, 3831-3832
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 3598 - 3599, The test code currently masks failures by using unwrap_or(wacore_binary::jid::Server::Lid) after wacore_binary::jid::Server::try_from(lid_server); replace those fallbacks with expect(...) so the conversion failure fails loudly and surfaces regressions — update the expression where Server::try_from(lid_server) is called (and the other identical occurrences in this file) to call expect with a clear message indicating the failed lid_server → Server mapping instead of defaulting to Server::Lid.src/client.rs (4)
1660-1667:⚠️ Potential issue | 🟠 MajorKeep awaited IQ responses zero-copy.
Line 1666 rematerializes the full stanza into
Node, so connect/app-state sync still pays a deep copy on every waiter hit. CarryOwnedNodeRef/Arc<OwnedNodeRef>throughresponse_waitersinstead of converting back here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1660 - 1667, The current code rematerializes the full stanza with node.to_owned_node() before sending to the waiter, causing a deep copy on every waiter hit; change the response_waiters map to hold OwnedNodeRef (or Arc<OwnedNodeRef>) instead of OwnedNode and update all insertion sites to store an OwnedNodeRef/Arc there so that in this block you simply clone/clone the Arc and send that (avoid calling node.to_owned_node()); adjust uses of response_waiters.lock().await.remove(id) and waiter.send(...) to accept/send the OwnedNodeRef/Arc type and update any types/traits (e.g., sender type) accordingly.
3188-3190:⚠️ Potential issue | 🟠 MajorAvoid cloning every non-ping IQ before pair dispatch.
Line 3189 unconditionally calls
node.to_owned(), which puts a deep copy back on the general IQ path even when the stanza has nothing to do with pairing. Either makepair::handle_iqconsumeNodeRef/OwnedNodeRef, or add a cheap pair-specific pre-check before materializing an ownedNode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3188 - 3190, The code currently always clones the stanza via node.to_owned() before calling pair::handle_iq, causing unnecessary copies; either change pair::handle_iq to accept a non-owning reference type (e.g., NodeRef or OwnedNodeRef) so it can operate without forcing a deep clone, or add a cheap pre-check (inspect node.tag/name/attrs on the NodeRef) right before the call and only call node.to_owned() when the stanza matches the pairing criteria; update the pair::handle_iq signature and call sites (or add the predicate check) accordingly so non-pair IQs avoid the allocation.
3628-3641: 🧹 Nitpick | 🔵 TrivialPreserve typed attrs in
build_ack_node.Lines 3629-3641 still stringify JID-backed attrs before rebuilding
NodeValue, so every ackable stanza pays extra formatting/allocation work in the ACK hot path. Ifwacore_binaryalready exposes a direct owned/value-preserving conversion, use that instead of theto_string_cow()round-trip.Run this read-only search to confirm whether a direct
ValueRef→NodeValuepath already exists:#!/bin/bash set -euo pipefail rg -n -C3 --type rust 'enum ValueRef|struct ValueRef|to_string_cow|impl .*From<.*ValueRef.*> for NodeValue|NodeValue::Jid' .Expected: locate a direct owned/value-preserving conversion helper in
wacore_binary; if one exists,build_ack_nodecan stop round-tripping these attrs through strings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3628 - 3641, The build_ack_node function currently forces JID-backed attributes (id, from, participant, and the type attr path) through to_string_cow() then reconstructs NodeValue, causing extra formatting/allocation on the ACK hot path; replace those round-trips by using the value-preserving conversion from wacore_binary's ValueRef (or the crate's direct impl From<ValueRef> for NodeValue / NodeValue::Jid helper) so you construct NodeValue directly from the attribute ValueRef (e.g., change map(|v| NodeValue::from(v.to_string_cow().as_ref())) to the direct conversion like NodeValue::from(v) or the crate-specific ValueRef→NodeValue API) for id, from, participant and the type branch, preserving typed values and avoiding string allocations.
4890-4896: 🧹 Nitpick | 🔵 TrivialMake the test helper follow the production unpack contract.
Lines 4892-4895 hardcode the current wire framing via
bytes[1..]. Usingmarshal_auto()pluswacore_binary::util::unpack()keeps this helper aligned if framing or compression behavior changes.Proposed fix
fn node_to_owned_ref(node: Node) -> Arc<wacore_binary::OwnedNodeRef> { - let bytes = wacore_binary::marshal::marshal(&node).expect("marshal should succeed"); - // marshal() prepends a leading 0x00 format byte; OwnedNodeRef::new expects raw protocol bytes without it + let bytes = wacore_binary::marshal::marshal_auto(&node).expect("marshal should succeed"); + let unpacked = wacore_binary::util::unpack(&bytes) + .expect("unpack should succeed") + .into_owned(); Arc::new( - wacore_binary::OwnedNodeRef::new(bytes[1..].to_vec()) + wacore_binary::OwnedNodeRef::new(unpacked) .expect("OwnedNodeRef::new should succeed"), ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4890 - 4896, The test helper node_to_owned_ref currently hardcodes framing by calling wacore_binary::marshal and slicing bytes[1..]; change it to use wacore_binary::marshal_auto(&node) and then pass the result through wacore_binary::util::unpack(...) to obtain the raw protocol payload, and then construct Arc::new(wacore_binary::OwnedNodeRef::new(unpacked).expect(...)). This ensures node_to_owned_ref follows the production unpack contract and will stay correct if framing/compression changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client/device_registry.rs`:
- Around line 240-241: The cleanup currently cross-products user aliases by
calling all_keys() and iterating both Server::Lid and Server::Pn, which throws
away the LID/PN distinction from resolve_lookup_keys() and can purge unrelated
accounts; update delete_sessions_for_devices() (and the similar block at
303-310) to match on resolve_lookup_keys() (UserLookupKeys::LidWithPn,
::PnWithLid, ::Unknown) and only call clear_device_record()/purge for the
specific (user, server) pairs returned (use the caller's server for the Unknown
case) rather than iterating both server variants or using all_keys(). Ensure
clear_device_record() is invoked with the exact (user, server) tuple from the
match so you preserve the original alias semantics.
In `@src/features/media_reupload.rs`:
- Around line 110-112: The code currently calls
notification_node.to_owned_node() which deep-clones the node and payload;
instead keep the borrowed path by adding an overload that accepts a borrowed
node reference (e.g., parse_media_retry_notification_borrowed or change
parse_media_retry_notification to accept &Node/NodeRef) and call that here with
the existing notification_node (avoid to_owned_node()); update the
parse_media_retry_notification implementation (or add a small wrapper) to
operate only on the borrowed stanza without requiring ownership so no deep clone
occurs.
In `@src/handlers/ib.rs`:
- Around line 113-116: The nested pattern matching around extracting
routing_info can be simplified with a let-chain: replace the two-level `if let`
(the call to `child.get_optional_child("routing_info")` binding
`routing_info_node` and the inner `if let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref()`) with a single `if let
Some(routing_info_node) = child.get_optional_child("routing_info") && let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref() { ... }` so that routing_info_node and
routing_bytes are bound in one condition (update the block body to use these
bindings).
In `@src/handlers/receipt.rs`:
- Line 28: The call currently does Arc::clone(&node) before
client.handle_receipt which unnecessarily bumps the Arc refcount; instead move
node into the call (client.handle_receipt(node).await) and remove the Arc::clone
usage. Update the handle_receipt signature if needed so it accepts the owned
Arc<Node> (by value) rather than borrowing, and ensure there are no subsequent
uses of node after this call so the move is valid.
In `@src/retry.rs`:
- Around line 57-64: The retry path currently materializes an owned Node inside
handle_retry_receipt (via node.to_owned_node()), which forces copying of the
retry stanza; change handle_retry_receipt to accept and work with a borrowed
node reference (e.g. &NodeRef<'_> or &OwnedNodeRef) and parse directly from
node.get() instead of calling to_owned_node(), and update/move any helper
functions that currently expect owned nodes to accept the borrowed NodeRef so no
allocation occurs when handling Receipt in handle_retry_receipt.
In `@wacore/binary/src/decoder.rs`:
- Around line 109-115: Add a regression test that exercises the new fail-fast
path in read_jid_pair: craft a minimal binary buffer representing a JID_PAIR
where the server string is invalid, feed it to the decoder (the same decoding
entry used in tests that ultimately calls read_jid_pair) and assert the decode
returns Err(BinaryError::AttrParse(..)); this ensures Server::try_from(...) in
read_jid_pair fails and the AttrParse error is produced rather than falling back
to the old Server::Pn behavior. Include references to read_jid_pair,
Server::try_from, and BinaryError::AttrParse in the test assertion so it fails
if the old fallback returns a valid Server instead of an error.
In `@wacore/src/iq/chatstate.rs`:
- Around line 140-145: The parse() path using attrs.optional_jid("from") treats
both missing and malformed JIDs the same because optional_jid() records parse
failures in attrs.errors and returns None, making
ChatstateParseError::InvalidJid unreachable; fix by distinguishing malformed vs
missing JID: after calling let mut attrs = node.attr_parser(); use either a
required-parsing helper (e.g., call attrs.jid("from") or similar) that returns a
parse error you can map to ChatstateParseError::InvalidJid, or, if you keep
optional_jid("from"), immediately inspect attrs.errors before returning
MissingFrom and return ChatstateParseError::InvalidJid when a JID parse error
for "from" is present (ensuring SelfEcho logic still checks
attrs.optional_jid("to") as before).
---
Outside diff comments:
In `@src/client/lid_pn.rs`:
- Around line 139-150: The code compares target.server to string constants
DEFAULT_USER_SERVER and HIDDEN_USER_SERVER; instead, use the typed Server enum
variants for consistency—replace checks against
wacore_binary::jid::DEFAULT_USER_SERVER and
wacore_binary::jid::HIDDEN_USER_SERVER with comparisons to
wacore_binary::jid::Server::Pn and wacore_binary::jid::Server::Lid respectively
(keeping the existing Jid construction that uses Server::Lid), and adjust any
imports or fully-qualify Server where needed so all server equality checks
consistently use the Server enum.
In `@src/handlers/ib.rs`:
- Around line 128-133: The direct mutation using
client_clone.persistence_manager.modify_device(...) to set
device.edge_routing_info = Some(routing_bytes) must be replaced by constructing
an appropriate DeviceCommand (e.g., SetEdgeRouting or similar) carrying
routing_bytes and sending it to
client_clone.persistence_manager.process_command(command). Also update reads to
use client_clone.persistence_manager.get_device_snapshot() where needed; ensure
you remove the direct modify_device call and use the
PersistenceManager::process_command() path so state changes go through
centralized command handling.
In `@src/message.rs`:
- Around line 73-90: The code silently returns when plaintext_node.content
exists but isn't NodeContentRef::Bytes; update the handling in the plaintext
block surrounding plaintext_node and wa::Message::decode so that if
plaintext_node.content.as_deref() is Some but not Bytes you emit a warning
(e.g., using log::warn!) that includes info.id and a brief description that
plaintext content was present but not bytes before returning, while keeping the
existing decode/dispatch_parsed_message flow for the Bytes case.
- Around line 366-372: The code repeatedly calls
self.custom_enc_handlers.read().await inside the per-<enc> node decode loop
which reacquires the async RwLock on every node; hoist the read guard outside
the inner node loop by calling let guard = self.custom_enc_handlers.read().await
once (or at the start of the multi-enc stanza), use
guard.get(enc_type.as_ref()).cloned() to fetch the handler, then drop the guard
(or let it go out of scope) before any subsequent await in the loop so the async
lock isn't held across awaits and isn't re-acquired per node; refer to
custom_enc_handlers, enc_type, and the handler variable when making this change.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 1660-1667: The current code rematerializes the full stanza with
node.to_owned_node() before sending to the waiter, causing a deep copy on every
waiter hit; change the response_waiters map to hold OwnedNodeRef (or
Arc<OwnedNodeRef>) instead of OwnedNode and update all insertion sites to store
an OwnedNodeRef/Arc there so that in this block you simply clone/clone the Arc
and send that (avoid calling node.to_owned_node()); adjust uses of
response_waiters.lock().await.remove(id) and waiter.send(...) to accept/send the
OwnedNodeRef/Arc type and update any types/traits (e.g., sender type)
accordingly.
- Around line 3188-3190: The code currently always clones the stanza via
node.to_owned() before calling pair::handle_iq, causing unnecessary copies;
either change pair::handle_iq to accept a non-owning reference type (e.g.,
NodeRef or OwnedNodeRef) so it can operate without forcing a deep clone, or add
a cheap pre-check (inspect node.tag/name/attrs on the NodeRef) right before the
call and only call node.to_owned() when the stanza matches the pairing criteria;
update the pair::handle_iq signature and call sites (or add the predicate check)
accordingly so non-pair IQs avoid the allocation.
- Around line 3628-3641: The build_ack_node function currently forces JID-backed
attributes (id, from, participant, and the type attr path) through
to_string_cow() then reconstructs NodeValue, causing extra formatting/allocation
on the ACK hot path; replace those round-trips by using the value-preserving
conversion from wacore_binary's ValueRef (or the crate's direct impl
From<ValueRef> for NodeValue / NodeValue::Jid helper) so you construct NodeValue
directly from the attribute ValueRef (e.g., change map(|v|
NodeValue::from(v.to_string_cow().as_ref())) to the direct conversion like
NodeValue::from(v) or the crate-specific ValueRef→NodeValue API) for id, from,
participant and the type branch, preserving typed values and avoiding string
allocations.
- Around line 4890-4896: The test helper node_to_owned_ref currently hardcodes
framing by calling wacore_binary::marshal and slicing bytes[1..]; change it to
use wacore_binary::marshal_auto(&node) and then pass the result through
wacore_binary::util::unpack(...) to obtain the raw protocol payload, and then
construct Arc::new(wacore_binary::OwnedNodeRef::new(unpacked).expect(...)). This
ensures node_to_owned_ref follows the production unpack contract and will stay
correct if framing/compression changes.
In `@src/handlers/notification.rs`:
- Around line 206-211: Several handlers currently call to_owned_node() which
forces allocations and prevents zero-copy; update the parsing/handler APIs to
accept a NodeRef<'_> (borrowed) and then pass &node (the existing NodeRef)
instead of calling to_owned_node() in call sites such as
crate::pair_code::handle_pair_code_notification, DeviceNotification::try_parse,
BusinessNotification::try_parse, and GroupNotification::try_from_node; change
those function signatures to take &NodeRef<'_> (or &impl AsNodeRef) and update
their internals to work with the borrowed node so you can remove the
to_owned_node() conversions across these notification handlers, tracking any
remaining unported call sites as follow-up work.
In `@src/message.rs`:
- Around line 3598-3599: The test code currently masks failures by using
unwrap_or(wacore_binary::jid::Server::Lid) after
wacore_binary::jid::Server::try_from(lid_server); replace those fallbacks with
expect(...) so the conversion failure fails loudly and surfaces regressions —
update the expression where Server::try_from(lid_server) is called (and the
other identical occurrences in this file) to call expect with a clear message
indicating the failed lid_server → Server mapping instead of defaulting to
Server::Lid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fa2e0ffa-15b7-4579-9f3b-fd44f17885fd
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (38)
Cargo.tomlsrc/client.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/features/media_reupload.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/receipt.rssrc/retry.rssrc/unified_session.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/src/iq/chatstate.rswacore/src/iq/privacy.rswacore/src/messages.rswacore/src/types/events.rs
| self.clear_device_record(user, device.jid.server.as_str(), &record) | ||
| .await; |
There was a problem hiding this comment.
Don't cross-product user aliases with both server variants.
resolve_lookup_keys() preserves which alias is LID vs PN, but delete_sessions_for_devices() throws that away via all_keys() and then loops both Server::Lid and Server::Pn. For a mapped user that purges four addresses, including mixed lid@s.whatsapp.net / pn@lid variants. Those are not the same account, and if a numeric LID collides with some real phone number, this cleanup can delete an unrelated Signal session. Match on UserLookupKeys and only purge valid (user, server) pairs; for the unknown case, use the caller's actual server instead of ignoring it in clear_device_record().
Suggested direction
match self.resolve_lookup_keys(user).await {
UserLookupKeys::LidWithPn { lid, pn } | UserLookupKeys::PnWithLid { lid, pn } => {
purge(&lid, wacore_binary::jid::Server::Lid, device_ids).await;
purge(&pn, wacore_binary::jid::Server::Pn, device_ids).await;
}
UserLookupKeys::Unknown { user } => {
purge(&user, server_from_caller, device_ids).await;
}
}Also applies to: 303-310
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/client/device_registry.rs` around lines 240 - 241, The cleanup currently
cross-products user aliases by calling all_keys() and iterating both Server::Lid
and Server::Pn, which throws away the LID/PN distinction from
resolve_lookup_keys() and can purge unrelated accounts; update
delete_sessions_for_devices() (and the similar block at 303-310) to match on
resolve_lookup_keys() (UserLookupKeys::LidWithPn, ::PnWithLid, ::Unknown) and
only call clear_device_record()/purge for the specific (user, server) pairs
returned (use the caller's server for the Unknown case) rather than iterating
both server variants or using all_keys(). Ensure clear_device_record() is
invoked with the exact (user, server) tuple from the match so you preserve the
original alias semantics.
| fn read_jid_pair(&mut self) -> Result<JidRef<'a>> { | ||
| let user_val = self.read_value_as_string()?; | ||
| let server = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); | ||
| let server_str = self.read_value_as_string()?.unwrap_or(Cow::Borrowed("")); | ||
| let user = user_val.unwrap_or(Cow::Borrowed("")); | ||
| let server = crate::jid::Server::try_from(server_str.as_ref()).map_err(|_| { | ||
| BinaryError::AttrParse(format!("JID_PAIR unknown server: {}", server_str)) | ||
| })?; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add a regression test for invalid JID_PAIR servers.
Lines 111-115 are the new fail-fast behavior behind the Server enum migration, but the decoder tests never exercise it. A tiny crafted buffer asserting BinaryError::AttrParse here would keep the old Server::Pn fallback from creeping back in.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/binary/src/decoder.rs` around lines 109 - 115, Add a regression test
that exercises the new fail-fast path in read_jid_pair: craft a minimal binary
buffer representing a JID_PAIR where the server string is invalid, feed it to
the decoder (the same decoding entry used in tests that ultimately calls
read_jid_pair) and assert the decode returns Err(BinaryError::AttrParse(..));
this ensures Server::try_from(...) in read_jid_pair fails and the AttrParse
error is produced rather than falling back to the old Server::Pn behavior.
Include references to read_jid_pair, Server::try_from, and
BinaryError::AttrParse in the test assertion so it fails if the old fallback
returns a valid Server instead of an error.
| let mut attrs = node.attr_parser(); | ||
| let from = match attrs.optional_jid("from") { | ||
| Some(jid) => jid, | ||
| None => { | ||
| if optional_jid(node, "to")?.is_some() { | ||
| if attrs.optional_jid("to").is_some() { | ||
| return Err(ChatstateParseError::SelfEcho); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify optional_jid API shape
rg -n "fn optional_jid\\(" wacore/binary/src -C2
# Verify InvalidJid construction sites in this parser
rg -n "InvalidJid|ChatstateParseError::InvalidJid" wacore/src/iq/chatstate.rs -C3Repository: jlucaso1/whatsapp-rust
Length of output: 1188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get full implementation of optional_jid method
echo "=== Full optional_jid implementation ==="
rg -n "pub fn optional_jid" wacore/binary/src/attrs.rs -A 10
# Get the full parse function context around line 140-145
echo -e "\n=== Full parse function in chatstate.rs ==="
rg -n "fn parse" wacore/src/iq/chatstate.rs -A 50 | head -80
# Check if InvalidJid is ever constructed in chatstate.rs
echo -e "\n=== All ChatstateParseError construction sites ==="
rg "ChatstateParseError::" wacore/src/iq/chatstate.rs -nRepository: jlucaso1/whatsapp-rust
Length of output: 3441
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if InvalidJid is constructed anywhere in the entire codebase
echo "=== InvalidJid construction across all files ==="
rg "ChatstateParseError::InvalidJid|InvalidJid\(" wacore/ -r
# Get the full ChatstateParseError enum definition
echo -e "\n=== ChatstateParseError enum definition ==="
rg -B2 "enum ChatstateParseError" wacore/src/iq/chatstate.rs -A 15
# Check if attr_parser().errors() is ever checked after optional_jid calls
echo -e "\n=== How errors from attr_parser are handled ==="
rg "attr_parser\(\).*errors\|\.errors()" wacore/src/iq/chatstate.rs -A 3 -B 3Repository: jlucaso1/whatsapp-rust
Length of output: 545
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if InvalidJid is constructed anywhere in the entire codebase
echo "=== InvalidJid construction across all files ==="
rg "ChatstateParseError::InvalidJid|InvalidJid\(" wacore/
# Get the full ChatstateParseError enum definition
echo -e "\n=== ChatstateParseError enum definition ==="
rg -B2 "enum ChatstateParseError" wacore/src/iq/chatstate.rs -A 15
# Check if errors from attr_parser are checked after optional_jid calls
echo -e "\n=== Check attr_parser error handling ==="
rg "attr_parser\(\)" wacore/src/iq/chatstate.rs -A 25 | head -40Repository: jlucaso1/whatsapp-rust
Length of output: 1826
InvalidJid is unreachable in parse().
optional_jid() swallows JID parse errors into the attrs.errors list and returns None for both missing and malformed attributes. Since the parser never checks attrs.errors, both cases collapse into MissingFrom. The InvalidJid variant exists in the enum but cannot be constructed, losing the error granularity it promises.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/iq/chatstate.rs` around lines 140 - 145, The parse() path using
attrs.optional_jid("from") treats both missing and malformed JIDs the same
because optional_jid() records parse failures in attrs.errors and returns None,
making ChatstateParseError::InvalidJid unreachable; fix by distinguishing
malformed vs missing JID: after calling let mut attrs = node.attr_parser(); use
either a required-parsing helper (e.g., call attrs.jid("from") or similar) that
returns a parse error you can map to ChatstateParseError::InvalidJid, or, if you
keep optional_jid("from"), immediately inspect attrs.errors before returning
MissingFrom and return ChatstateParseError::InvalidJid when a JID parse error
for "from" is present (ensuring SelfEcho logic still checks
attrs.optional_jid("to") as before).
d4f7e86 to
a82a3ea
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a82a3ea468
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .map(|from| from == SERVER_JID) | ||
| .unwrap_or(false) | ||
| { | ||
| if node.get_attr("from").and_then(|v| v.as_str()) != Some(SERVER_JID) { |
There was a problem hiding this comment.
Accept JID-valued
from when routing pairing IQs
ValueRef::as_str() returns None for ValueRef::Jid, so this guard now rejects IQ stanzas whose from attribute is decoded as a structured JID token. In that case handle_iq exits early and skips all pairing branches (pair-device, pair-success, etc.), which can break QR/login pairing flows depending on wire encoding. The previous comparison path handled both string and JID representations.
Useful? React with 👍 / 👎.
|
|
||
| /// Builds acknowledgment node for a pairing request from a NodeRef. | ||
| pub fn build_ack_node_ref(request_node: &NodeRef<'_>) -> Option<Node> { | ||
| let to = request_node.get_attr("from").and_then(|v| v.as_str())?; |
There was a problem hiding this comment.
Build pair ACKs from JID-valued
from attrs
This ACK builder now requires from to be a plain string via as_str(), but decoded attributes may be ValueRef::Jid for the same wire field. When that happens, build_ack_node_ref returns None and no IQ result ACK is sent for pairing requests, which can stall or fail pairing handshakes on JID-token encoded payloads.
Useful? React with 👍 / 👎.
a82a3ea to
80febd5
Compare
|
@claude review changes, check for dry imrovemnts, i think this PR has so much additions over remove, can we do things better? Also try to find dead code, like utils and functions that we need to aoid because of the new behaviour, this will cleanup code and make users less confusedo f what utilizies (also devs) |
d523c68 to
ff7c639
Compare
|
@claude review things carefully |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
ff7c639 to
2fe06ef
Compare
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
wacore/src/xml.rs (1)
67-74:⚠️ Potential issue | 🟡 MinorInconsistent attribute ordering between
NodeandNodeRefformatting.The
Node::format_attributesimplementation (lines 29-41) sorts attribute keys before formatting, but thisNodeRef::format_attributesimplementation iterates directly without sorting. This means the same logical node will produce different XML output depending on whether it's aNodeorNodeRef, which could cause issues for tests, debugging, or any code expecting deterministic output.Consider making the behavior consistent by sorting attributes here as well.
📋 Proposed fix to add sorting for consistent output
fn format_attributes(&self, out: &mut String) { if self.attrs.is_empty() { return; } - for (key, value) in self.attrs.iter() { + let mut attrs_vec: Vec<_> = self.attrs.iter().collect(); + attrs_vec.sort_unstable_by_key(|(k, _)| *k); + for (key, value) in attrs_vec { let _ = write!(out, " {}=\"{}\"", key, value); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/xml.rs` around lines 67 - 74, NodeRef::format_attributes currently iterates self.attrs directly causing nondeterministic attribute ordering; make it consistent with Node::format_attributes by collecting the keys, sorting them, and then writing attributes in sorted order (e.g., mirror the sorting logic used in Node::format_attributes when formatting into out) so both Node and NodeRef produce deterministic, identical XML output for the same attributes.wacore/src/iq/props.rs (1)
191-216: 🧹 Nitpick | 🔵 TrivialAvoid trial-parsing both
propvariants on the hot path.
AbPropConfig::try_from_node_refcurrently attempts a fullAbPropparse and then a fullSamplingPropparse for the same node. That adds redundant attribute lookups for large props payloads, and it can also accept ambiguous mixed payloads instead of rejecting them explicitly. Dispatch off the discriminator attrs first, then parse exactly one variant.♻️ Suggested refactor
fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { + use crate::iq::node::optional_attr; + if node.tag != "prop" { return Err(anyhow::anyhow!("expected <prop>, got <{}>", node.tag)); } - - let experiment = AbProp::try_from_node_ref(node); - if let Ok(prop) = experiment { - return Ok(Self::Experiment(prop)); - } - - let sampling = SamplingProp::try_from_node_ref(node); - if let Ok(prop) = sampling { - return Ok(Self::Sampling(prop)); - } - - let experiment_err = experiment - .err() - .unwrap_or_else(|| anyhow::anyhow!("unknown error")); - let sampling_err = sampling - .err() - .unwrap_or_else(|| anyhow::anyhow!("unknown error")); - Err(anyhow::anyhow!( - "prop did not match experiment or sampling config: experiment_err={}; sampling_err={}", - experiment_err, - sampling_err - )) + match ( + optional_attr(node, "config_code"), + optional_attr(node, "event_code"), + ) { + (Some(_), None) => AbProp::try_from_node_ref(node).map(Self::Experiment), + (None, Some(_)) => SamplingProp::try_from_node_ref(node).map(Self::Sampling), + (Some(_), Some(_)) => Err(anyhow::anyhow!( + "prop cannot contain both experiment and sampling attributes" + )), + (None, None) => Err(anyhow::anyhow!( + "prop missing discriminator attributes" + )), + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/props.rs` around lines 191 - 216, Refactor try_from_node_ref to dispatch on discriminator attributes before parsing: inspect the node's distinguishing attribute(s) (e.g., the attribute(s) that indicate experiment vs sampling) and if they clearly indicate AbProp -> call AbProp::try_from_node_ref(node) and return Self::Experiment on success; if they indicate Sampling -> call SamplingProp::try_from_node_ref(node) and return Self::Sampling on success; if both discriminators are present treat as ambiguous and return a clear error, and if neither present return a "missing discriminator" error. Ensure you remove the current pattern of invoking both AbProp::try_from_node_ref and SamplingProp::try_from_node_ref unconditionally so attribute lookups happen only once and mixed payloads are rejected explicitly.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorPropagate malformed
<host>entries instead of silently dropping them.
filter_map(... .ok()?)turns host-parse failures into omissions, so a partially invalidmedia_connresponse now succeeds with a truncated or empty host list. That masks protocol regressions and can leave callers with no usable media endpoint even though the IQ parse reported success.MediaConnResponseExtended::try_from_node_refalready fails fast here; this path should do the same.🐛 Suggested fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(|host_node| { + let ext = MediaConnHostExtended::try_from_node_ref(host_node)?; + Ok(MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<_, anyhow::Error>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The current parsing silently drops malformed <host> entries because filter_map(... .ok()?) swallows MediaConnHostExtended::try_from_node_ref errors; change the chain to map each host_node -> MediaConnHostExtended::try_from_node_ref(host_node) and then map Ok(ext) into MediaConnHost, collect into a Result<Vec<MediaConnHost>, _> (so use collect::<Result<_, _>>() or equivalent) and propagate the error with ? so that parsing fails fast (affecting the hosts construction where media_conn_node.get_children_by_tag("host") is used and the MediaConnHostExtended::try_from_node_ref call).wacore/src/iq/prekeys.rs (1)
62-74:⚠️ Potential issue | 🟠 MajorDon't coerce malformed integer payloads to
0.This helper zero-fills missing/non-byte content and truncates oversized payloads.
DigestKeyBundleSpec::parse_responseuses it for required fields like<registration>and<skey><id>, so a corrupt server response becomes a valid-looking bundle instead of failing fast.🛠️ Safer direction
-fn extract_content_uint(node: Option<&NodeRef<'_>>) -> u32 { - node.and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) => { - let mut buf = [0u8; 4]; - let len = b.len().min(4); - buf[4 - len..].copy_from_slice(&b[..len]); - Some(u32::from_be_bytes(buf)) - } - _ => None, - }) - .unwrap_or(0) +fn extract_required_content_uint( + node: Option<&NodeRef<'_>>, + tag: &str, +) -> Result<u32, anyhow::Error> { + let bytes = match node.and_then(|n| n.content.as_deref()) { + Some(NodeContentRef::Bytes(b)) if (1..=4).contains(&b.len()) => b, + _ => return Err(anyhow!("missing or invalid bytes in <{}>", tag)), + }; + let mut buf = [0u8; 4]; + buf[4 - bytes.len()..].copy_from_slice(bytes); + Ok(u32::from_be_bytes(buf)) }wacore/derive/src/lib.rs (1)
184-270: 🧹 Nitpick | 🔵 TrivialReuse one
attr_parser()per generated parser.The derive now emits
node.attr_parser()once per field access. On larger stanzas that adds avoidable work to the decode hot path; generate a single local parser and read every attribute from it instead.♻️ Generated shape to target
fn try_from_node_ref(node: &::wacore_binary::node::NodeRef<'_>) -> ::anyhow::Result<Self> { if node.tag != `#tag` { return Err(::anyhow::anyhow!("expected <{}>, got <{}>", `#tag`, node.tag)); } + let mut attrs = node.attr_parser(); Ok(Self { - `#field_ident`: node.attr_parser().required_string(`#attr_name`)?.to_string(), + `#field_ident`: attrs.required_string(`#attr_name`)?.to_string(), #(`#field_parsers`),* }) }Also applies to: 329-335
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/derive/src/lib.rs` around lines 184 - 270, The generated code currently calls node.attr_parser() for every attribute access (see match arms that emit expressions using node.attr_parser()), which is wasteful; change the generator to emit a single local let parser = node.attr_parser(); at the start of the generated parser body and then replace all occurrences of node.attr_parser() in the AttrType match arms with parser so every attribute read (optional_jid, required_string, optional_string, optional_u64, etc.) uses that single parser instance; ensure this same change is applied for the other block referenced (lines 329-335) so both places reuse the local parser variable.src/handlers/notification.rs (1)
1217-1296: 🧹 Nitpick | 🔵 TrivialHoist the group-cache lookup out of the action loop.
These notifications can batch multiple actions, but
get_group_cache().awaitis still paid once per action. Pulling it up once per notification keeps this path closer to the perf goal of the PR.♻️ Minimal diff
let timestamp = i64::try_from(notification.timestamp) .ok() .and_then(|t| chrono::DateTime::from_timestamp(t, 0)) .unwrap_or_else(chrono::Utc::now); + let group_cache = client.get_group_cache().await; for action in notification.actions { // Granularly patch group cache instead of invalidating — matches WA Web's // addParticipantInfo / removeParticipantInfo pattern and avoids a // group metadata IQ round-trip. match &action { GroupNotificationAction::Add { participants, .. } => { - let group_cache = client.get_group_cache().await; if let Some(mut info) = group_cache.get(¬ification.group_jid).await { let new: Vec<_> = participants .iter() .map(|p| (p.jid.clone(), p.phone_number.clone())) .collect(); @@ } GroupNotificationAction::Remove { participants, .. } => { - let group_cache = client.get_group_cache().await; if let Some(mut info) = group_cache.get(¬ification.group_jid).await { let users: Vec<&str> = participants.iter().map(|p| p.jid.user.as_str()).collect();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/notification.rs` around lines 1217 - 1296, The group-cache lookup is performed inside the actions loop causing an unnecessary await per action; in handle_group_notification, call client.get_group_cache().await once before iterating notification.actions and reuse that group_cache variable inside the match arms (refer to handle_group_notification, get_group_cache, and notification.actions) so you remove the per-action awaits and still call group_cache.get(...).await / group_cache.insert(...).await as before; ensure the moved variable is in scope for both Add and Remove arms and adjust mutability if needed.src/retry.rs (1)
551-567: 🧹 Nitpick | 🔵 TrivialDeduplicate the registration-ID decoding.
The variable-length big-endian parsing now exists in three places (
extract_registration_id_from_node,extract_registration_id_from_node_ref, and this closure). Pull it into one helper so the test-only owned path and the productionNodeRefpath cannot drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/retry.rs` around lines 551 - 567, The registration-ID decoding logic is duplicated across extract_registration_id_from_node, extract_registration_id_from_node_ref, and the closure that maps registration_node.and_then(get_bytes_content_ref); extract that variable-length big-endian parse into a single helper (e.g., decode_registration_id_from_bytes or parse_be_u32_varlen) that takes a &[u8] and returns a u32, then replace the logic in extract_registration_id_from_node, extract_registration_id_from_node_ref, and the registration_node mapping to call that helper (use get_bytes_content_ref to obtain the &[u8] before calling the helper).src/client.rs (2)
2272-2317:⚠️ Potential issue | 🟠 MajorACK waiters still re-encode and re-parse the stanza.
Lines 2303-2308 marshal the borrowed node back into bytes, strip the format byte manually, and parse it again into
OwnedNodeRef. ACKs are on a common send path, so this adds avoidable CPU/allocation churn and duplicates framing knowledge inbytes[1..]. Thread the originalArc<OwnedNodeRef>intohandle_ack_responseand forward it directly, like the IQ waiter path already does.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 2272 - 2317, handle_ack_response is re-encoding the borrowed NodeRef then reparsing into an OwnedNodeRef, causing unnecessary CPU/allocations; change handle_ack_response to accept and forward the original Arc<wacore_binary::OwnedNodeRef> (or an Arc-wrapped owned ref) instead of a borrowed NodeRef, remove the marshal_ref/bytes[1..] re-encode path, and send that Arc directly to the waiter (the same pattern as the IQ waiter). Update the caller(s) that invoke handle_ack_response to pass the Arc<OwnedNodeRef> they already have, and keep the response_waiters removal/send logic using waiter.send(arc_onr) so no framing/encoding is duplicated.
1503-1514:⚠️ Potential issue | 🟠 Major
into_owned()puts a full frame copy back on the receive hot path.When
util::unpack()returns a borrowed slice for an uncompressed stanza, Line 1512 clones the entire payload anyway. That reintroduces O(n) allocation/copy work in the common case and undercuts the zero-copy decode goal. Handle the borrowed branch by reusing or mutatingdecrypted_payloadinstead of always materializing a newVec.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1503 - 1514, The current code always calls into_owned() on unpacked_data_cow which clones the frame; instead match unpacked_data_cow and avoid cloning when it's a borrowed slice: if unpacked_data_cow is Cow::Borrowed(_), reuse or mutate the existing decrypted_payload Vec (consume decrypted_payload and adjust/truncate it to the borrowed slice bytes) and pass that Vec into wacore_binary::OwnedNodeRef::new; if it's Cow::Owned(vec) only then use that owned vec (or into_owned())—update the logic around unpacked_data_cow, decrypted_payload, and the call to wacore_binary::OwnedNodeRef::new to select the appropriate Vec without an unnecessary copy.
♻️ Duplicate comments (5)
src/handlers/receipt.rs (1)
28-28: 🧹 Nitpick | 🔵 TrivialMove
nodeintohandle_receiptinstead of cloning it.The earlier type mismatch is fixed, but this still does an unnecessary
Arcrefcount bump on every receipt even thoughnodeis never used again after the call.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/receipt.rs` at line 28, The call currently does an unnecessary Arc refcount bump: replace client.handle_receipt(Arc::clone(&node)).await with a move of the Arc (client.handle_receipt(node).await) and update handle_receipt's signature to accept the Arc by value (e.g., fn handle_receipt(&self, node: Arc<Node>) / async fn handle_receipt(&self, node: Arc<Node>) as appropriate) so the Arc is moved instead of cloned; also remove any later uses of `node` in this scope (or recreate it if needed) so there are no borrow/use-after-move issues.src/handlers/ib.rs (1)
113-143: 🧹 Nitpick | 🔵 TrivialCollapse the
routing_infoextraction into a let-chain.This reintroduces the nested
if letpattern the repo explicitly avoids. Bindrouting_info_nodeandrouting_bytesin one condition and keep the fallback logging in theelsebranches.♻️ Suggested refactor
- if let Some(routing_info_node) = child.get_optional_child("routing_info") { - if let Some(NodeContentRef::Bytes(routing_bytes)) = - routing_info_node.content.as_deref() - { + let routing_info_node = child.get_optional_child("routing_info"); + if let Some(routing_info_node) = routing_info_node + && let Some(NodeContentRef::Bytes(routing_bytes)) = + routing_info_node.content.as_deref() + { if !routing_bytes.is_empty() { debug!( "Received edge routing info ({} bytes), storing for reconnection", routing_bytes.len() ); // Spawn to avoid blocking the read loop on Device write lock. let routing_bytes = routing_bytes.to_vec(); let client_clone = client.clone(); client .runtime .spawn(Box::pin(async move { client_clone .persistence_manager .modify_device(|device| { device.edge_routing_info = Some(routing_bytes); }) .await; })) .detach(); } else { debug!("Received empty edge routing info, ignoring"); } - } else { - debug!("Edge routing info node has no bytes content"); - } - } else { - debug!("Edge routing stanza has no routing_info child"); - } + } else if routing_info_node.is_none() { + debug!("Edge routing stanza has no routing_info child"); + } else { + debug!("Edge routing info node has no bytes content"); + }As per coding guidelines "Use let-chains (
if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks to maintain collapsible if patterns".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 113 - 143, Collapse the nested if-let into a single let-chain: combine the checks for routing_info_node and routing_bytes using `if let Some(routing_info_node) = child.get_optional_child("routing_info") && let Some(NodeContentRef::Bytes(routing_bytes)) = routing_info_node.content.as_deref() { ... }`, keep the existing logic that checks for empty bytes and spawns the async task (referencing client.runtime.spawn, client.persistence_manager.modify_device, and device.edge_routing_info) inside the true branch, and preserve the two fallback debug logs in the appropriate else branches (one for missing routing_info child and one for non-bytes/empty content).src/client/device_registry.rs (1)
301-313:⚠️ Potential issue | 🟠 MajorOnly purge valid
(user, server)pairs here.This still cross-products
lookup.all_keys()with bothServer::LidandServer::Pn, so a mapped user now purges synthetic addresses likepn@lidandlid@s.whatsapp.net. That can delete unrelated Signal sessions, and it also ignores the caller’s actual server forUserLookupKeys::Unknown. Match onUserLookupKeysand emit only the real pairs instead.♻️ Suggested direction
- async fn delete_sessions_for_devices(&self, user: &str, device_ids: &[u16]) { + async fn delete_sessions_for_devices( + &self, + user: &str, + unknown_server: wacore_binary::Server, + device_ids: &[u16], + ) { let lookup = self.resolve_lookup_keys(user).await; - let servers = [wacore_binary::Server::Lid, wacore_binary::Server::Pn]; - for server in servers { - for key in lookup.all_keys() { - for &device_id in device_ids { - let mut jid = Jid::new(key, server); - jid.device = device_id; - let addr = wacore::types::jid::JidExt::to_protocol_address(&jid); - self.signal_cache.delete_session(&addr).await; - } - } - } + let aliases: Vec<(String, wacore_binary::Server)> = match lookup { + UserLookupKeys::LidWithPn { lid, pn } | UserLookupKeys::PnWithLid { lid, pn } => vec![ + (lid, wacore_binary::Server::Lid), + (pn, wacore_binary::Server::Pn), + ], + UserLookupKeys::Unknown { user } => vec![(user, unknown_server)], + }; + + for (key, server) in aliases { + for &device_id in device_ids { + let mut jid = Jid::new(&key, server); + jid.device = device_id; + let addr = wacore::types::jid::JidExt::to_protocol_address(&jid); + self.signal_cache.delete_session(&addr).await; + } + } self.flush_signal_cache_logged("delete_sessions_for_devices", None) .await; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/device_registry.rs` around lines 301 - 313, The current delete_sessions_for_devices blindly cross-products lookup.all_keys() with both Server::Lid and Server::Pn, causing invalid (user,server) pairs to be purged; change delete_sessions_for_devices to match on each lookup key (from resolve_lookup_keys / UserLookupKeys) and emit only the actual server(s) for that key (e.g., map Lid->Server::Lid, Pn->Server::Pn, and for UserLookupKeys::Unknown use the caller-provided server), then construct Jid with that single server and call self.signal_cache.delete_session(&addr).await for each device_id — do not iterate the fixed servers array.wacore/src/iq/chatstate.rs (1)
140-145:⚠️ Potential issue | 🟡 Minor
InvalidJidis still unreachable here.
optional_jid("from")returnsNonefor both missing and malformed values, and this parser never inspects the accumulated attr errors, so badfromJIDs still collapse intoMissingFrom/SelfEcho. This is the same issue raised in the previous review.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/chatstate.rs` around lines 140 - 145, The current parsing for `from` in the chatstate parsing block uses `attrs.optional_jid("from")` which returns None for both missing and malformed JIDs, so malformed JIDs are being treated as `MissingFrom`/`SelfEcho`; change the logic around the `let from = match attrs.optional_jid("from") { ... }` in `chatstate.rs` to detect and map malformed JIDs to `ChatstateParseError::InvalidJid` instead of collapsing them: either use the attribute parser API that returns a Result (e.g., `attrs.jid("from")` or `attrs.require_jid("from")`) so you can match an `Err(InvalidJid)` and return `ChatstateParseError::InvalidJid`, or, if only `optional_jid` is available, inspect the parser’s accumulated attribute errors (e.g., `attrs.errors()` or equivalent) after calling `optional_jid("from")` and return `ChatstateParseError::InvalidJid` when a malformed- JID error for the "from" attribute is present; ensure the `SelfEcho`/`MissingFrom` branches remain for the genuinely missing case.src/client.rs (1)
3640-3653: 🛠️ Refactor suggestion | 🟠 MajorACK building still stringifies typed attrs.
Lines 3641-3653 run
from/participant/typethroughto_string_cow()before wrapping them back intoNodeValue, so JID-backed attrs still pay the string round-trip on every ackable stanza. Preserve the borrowed value/JID directly if theNodeValueAPI supports it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3640 - 3653, In build_ack_node, avoid stringifying JID-backed attributes by removing the to_string_cow() round-trip for "from", "participant" and the "type" branch; instead construct NodeValue directly from the borrowed attribute/JID returned by node.get_attr(...) (using the NodeValue API overload that accepts the borrowed/cow or Jid-backed value) so you preserve the original borrowed/JID representation and eliminate unnecessary allocations when building ack nodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/handlers/notification.rs`:
- Around line 557-560: The current mapping silently truncates key-index with `v
as u32`, so change the conversion in the AccountSyncDevice construction (the
closure using attr_parser().optional_u64("key-index")) to use fallible
conversion: replace the cast with a try conversion like u32::try_from(v).ok() so
overflowing u64 values become None and are safely ignored by the surrounding
filter_map; keep the rest of the AccountSyncDevice fields (jid via optional_jid)
unchanged.
In `@src/message.rs`:
- Around line 376-377: The hotspot is that (*enc_node).to_owned()
re-materializes the full <enc> payload (enc_node_owned) before calling custom
enc handlers; instead change the handler API to accept a reference (e.g.,
&NodeRef or &OwnedNodeRef) so we can pass a borrow and avoid allocation.
Concretely: update the enc handler trait/signature to take &NodeRef (or
&OwnedNodeRef) instead of owning Node, update all implementations/call-sites to
accept the borrowed type, and remove the to_owned() allocation at the call site
(use a borrow of enc_node instead). Ensure any places that previously relied on
ownership either clone only when strictly required or adjust to OwnedNodeRef for
explicit ownership.
In `@src/pair_code.rs`:
- Around line 238-245: The branch that extracts the wrapped ephemeral currently
returns a Vec (primary_wrapped_ephemeral) even though you already require
exactly 80 bytes; change it to produce a fixed-size array [u8; 80] instead of
Vec<u8>. Locate the match on
reg_node.get_optional_child_by_tag(...).and_then(|n| match n.content.as_deref()
{ Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), _ =>
None, }) and replace the Some(b.to_vec()) path with code that converts the slice
into a [u8; 80] (e.g., use try_into() on b and propagate None on failure) so
primary_wrapped_ephemeral becomes a [u8; 80] and can be passed directly to the
downstream blocking decrypt call without heap allocation.
In `@src/pair.rs`:
- Around line 54-56: The code is allocating early with
String::from_utf8(bytes.to_vec()); instead, keep it zero-copy by validating
UTF-8 with std::str::from_utf8 on the borrowed bytes and only allocate at the
end if needed. Replace the pattern in the NodeContentRef::Bytes match (the
grandchild.content branch) to call std::str::from_utf8(bytes.as_ref()) (or
bytes.as_slice()) and then, only on Ok(r), convert r to an owned String (e.g.,
r.to_string() or String::from(r)) where the owned value is actually required.
- Around line 201-206: The current code silently replaces missing or malformed
device jid/lid with Jid::default() by using
optional_jid(...).unwrap_or_default(), which allows invalid pair-success
messages to proceed and persist empty identifiers; change the logic in the
pair-success handling (the block that calls
success_node.get_optional_child_by_tag("device") and uses
device_node.attr_parser() to parse "jid" and "lid") to validate both attributes
strictly: attempt to parse each with optional_jid (or the parser's parsing
method) and if either returns None or an Err, immediately return an error
rejecting the pair-success instead of defaulting, preventing issuing
SetId/SetLid commands with empty JIDs and matching the strict validation used
for device-identity.
In `@src/test_utils.rs`:
- Around line 8-10: The code in node_to_owned_ref currently allocates a second
buffer with bytes[1..].to_vec(); instead remove the leading format byte in-place
to avoid the extra allocation: assert that bytes[0] == 0x00 (or use an explicit
expect message), call bytes.remove(0) to strip the first byte, then pass the
mutated bytes into OwnedNodeRef::new (keep the existing expect on
OwnedNodeRef::new). Refer to marshal (wacore_binary::marshal::marshal),
node_to_owned_ref, and OwnedNodeRef::new when making this change.
In `@wacore/appstate/src/patch_decode.rs`:
- Around line 89-99: The two zero-copy entry points parse_patch_list_ref and
parse_patch_lists_ref currently call node.to_owned() which allocates and defeats
zero-copy; change them to parse directly from &NodeRef by either (a) refactoring
parse_patch_list and parse_patch_lists to accept &NodeRef<'_> (update their
signatures and callers) or (b) add internal helper functions (e.g.,
parse_patch_list_from_ref and parse_patch_lists_from_ref) that take &NodeRef<'_>
and reuse the existing parsing logic (calling the existing parse_* variants if
necessary) so you can remove the .to_owned() calls and parse without cloning the
node tree. Ensure all uses of Node methods (get_optional_child_by_tag,
get_optional_child, children) operate on NodeRef.
In `@wacore/binary/src/jid.rs`:
- Around line 508-512: The actual_agent() method currently only treats
Server::Pn as agent-less; update it so that it returns 0 when self.server is any
of the agent-less variants (Server::Pn, Server::Lid, Server::Hosted,
Server::HostedLid) and otherwise returns self.agent; modify the match in
actual_agent() to include those additional Server variants mapping to 0 to
prevent hidden agent state on manually-constructed JIDs.
In `@wacore/src/appstate_sync.rs`:
- Around line 98-156: The decode_patch_list_ref and decode_patch_list functions
duplicate the snapshot and external_mutations download/hydration flow; refactor
by extracting that shared logic into a single helper (e.g., process_patch_lists)
that accepts a mutable PatchList (or a mutable ref wrapper) plus the download
callback and validate_macs flag, performs the snapshot_ref download/decoding and
per-patch external_mutations download/decoding (preserving the existing log
behavior and patch.mutations replacement), and then calls
self.process_patch_list(pl, validate_macs). Update decode_patch_list_ref and
decode_patch_list to build/obtain the PatchList, call the new helper, and return
its Result to avoid drift between the two code paths.
In `@wacore/src/iq/blocklist.rs`:
- Around line 116-131: The parse_response implementation duplicates
BlocklistResponse's parsing logic; replace the manual <list>/<item> traversal
with a call to BlocklistResponse::try_from_node_ref(response), propagate or map
its Result into the expected Ok(entries) shape, and preserve the existing warn
behavior on parse error (e.g., warn! target: "blocklist" with the error) so
failures are logged the same way; update references in fn parse_response to use
BlocklistResponse::try_from_node_ref instead of
BlocklistEntry::try_from_node_ref and collect entries from the returned
BlocklistResponse.
In `@wacore/src/iq/business.rs`:
- Around line 55-59: The match arm handling NodeContentRef::Bytes in node_text
performs an extra allocation by cloning bytes into a Vec before UTF-8 checking;
instead, validate the borrowed byte slice directly (use std::str::from_utf8 on
the &\[u8\]) and then allocate once by converting the resulting &str to a String
(e.g., call to_string()/to_owned() on the validated &str), replacing the
String::from_utf8(b.to_vec()).ok() expression in the node_text function.
In `@wacore/src/iq/groups.rs`:
- Around line 379-382: The current code defaults unknown participant types to
ParticipantType::Member which silently downgrades privileges; change the
participant_type binding so that if attrs.optional_string("type") is None you
keep ParticipantType::Member, but if it is Some(s) you must call
ParticipantType::try_from(s.as_ref()) and propagate or return an error when
try_from fails instead of using unwrap_or; update the code that defines
participant_type (the attrs.optional_string / ParticipantType::try_from usage)
to return a parse error (or Result::Err) on unknown values so unknown types are
not coerced to Member.
In `@wacore/src/iq/node.rs`:
- Around line 22-25: Change required_attr to return Result<Cow<'_, str>,
anyhow::Error> instead of String and avoid the eager to_string allocation: keep
the map from node.get_attr(key) but map the borrowed &str into Cow::Borrowed (or
use Cow::from) rather than calling to_string, and keep the same ok_or_else(...)
error path; update the function signature to pub(crate) fn required_attr(node:
&NodeRef<'_>, key: &str) -> Result<Cow<'_, str>, anyhow::Error> and add use
std::borrow::Cow; also update any callers that expect String to accept or
convert the Cow.
In `@wacore/src/iq/tctoken.rs`:
- Around line 281-287: The code is allocating and reparsing the JID string from
token_node by calling get_attr(...).to_string_cow().into_owned().parse();
instead use the typed attribute path ValueRef::to_jid() to avoid the extra
allocation/parse. Replace the get_attr -> to_string_cow -> parse flow with
token_node.get_attr("jid").and_then(|v| v.to_jid()) (or the equivalent API) so
you obtain a Jid directly and propagate the same error handling (convert missing
attribute to the existing "missing required attribute jid" error and map to the
same "invalid jid" style error when to_jid fails) while keeping symbols
token_node, get_attr, and Jid referenced.
In `@wacore/src/iq/usync.rs`:
- Around line 121-128: The current parsing in parse_lid_jid uses
attr_parser().optional_string("val").and_then(|val| val.parse::<Jid>().ok())
which forces allocation and re-parsing when the decoder already provides a Jid;
replace that pattern with attr_parser().optional_jid("val") to avoid needless
to_string/parse. Apply the same change to the other occurrences mentioned (the
similar parsing blocks around the parse user/jid logic at the other sites
referenced) so they call optional_jid("...") instead of
optional_string(...).and_then(|s| s.parse::<Jid>().ok()), keeping return types
as Option<Jid>.
In `@wacore/src/pair_code.rs`:
- Around line 32-34: Replace string-based SERVER_JID usage with the typed Server
enum: import wacore_binary::Server and change occurrences of
SERVER_JID.to_string() in the pair-code IQ builders to use Server::Pn (pass the
enum value instead of the string). Update the builder calls that currently
expect a String/&str to accept Server::Pn (or call .to_string() only if the API
strictly requires a String) — adjust the function signature or overloads where
needed so functions like the pair-code IQ constructors accept Server::Pn instead
of the SERVER_JID string constant.
In `@wacore/src/pair.rs`:
- Around line 85-98: build_ack_node_ref duplicates build_ack_node; extract a
single private builder that takes (to: &str, id: &str) and returns the Node,
then have both build_ack_node_ref and build_ack_node call that shared helper.
Implement something like a private fn (e.g., build_ack_inner or build_ack_for)
that constructs the Node via NodeBuilder using the provided &strs, and update
build_ack_node_ref to map attrs to &str and pass them through, and update
build_ack_node to forward its owned Strings to the same helper (converting to
&str as needed) so there is a single place to change the ACK shape.
In `@wacore/src/prekeys.rs`:
- Around line 144-147: The parser currently calls to_vec() on
NodeContentRef::Bytes (in extract_bytes_ref and similar branches) which
allocates; instead return a borrowed slice (e.g., Result<&[u8], anyhow::Error>
from extract_bytes_ref or add an extract_bytes_ref_slice that returns &[u8]) and
propagate that borrowed slice through the registration, identity key, prekey,
and signature parsing code paths; only at the final boundary, when you must
produce a fixed-size array, validate the slice length and copy into the stack
array (e.g., using try_into().map_err(...) and copying into [u8; N]) so no
intermediate Vec is allocated. Ensure you update call sites that expect Vec<u8>
to accept &[u8] or perform the single copy at the end in functions handling
registration, identity keys, prekeys, and signatures.
---
Outside diff comments:
In `@src/client.rs`:
- Around line 2272-2317: handle_ack_response is re-encoding the borrowed NodeRef
then reparsing into an OwnedNodeRef, causing unnecessary CPU/allocations; change
handle_ack_response to accept and forward the original
Arc<wacore_binary::OwnedNodeRef> (or an Arc-wrapped owned ref) instead of a
borrowed NodeRef, remove the marshal_ref/bytes[1..] re-encode path, and send
that Arc directly to the waiter (the same pattern as the IQ waiter). Update the
caller(s) that invoke handle_ack_response to pass the Arc<OwnedNodeRef> they
already have, and keep the response_waiters removal/send logic using
waiter.send(arc_onr) so no framing/encoding is duplicated.
- Around line 1503-1514: The current code always calls into_owned() on
unpacked_data_cow which clones the frame; instead match unpacked_data_cow and
avoid cloning when it's a borrowed slice: if unpacked_data_cow is
Cow::Borrowed(_), reuse or mutate the existing decrypted_payload Vec (consume
decrypted_payload and adjust/truncate it to the borrowed slice bytes) and pass
that Vec into wacore_binary::OwnedNodeRef::new; if it's Cow::Owned(vec) only
then use that owned vec (or into_owned())—update the logic around
unpacked_data_cow, decrypted_payload, and the call to
wacore_binary::OwnedNodeRef::new to select the appropriate Vec without an
unnecessary copy.
In `@src/handlers/notification.rs`:
- Around line 1217-1296: The group-cache lookup is performed inside the actions
loop causing an unnecessary await per action; in handle_group_notification, call
client.get_group_cache().await once before iterating notification.actions and
reuse that group_cache variable inside the match arms (refer to
handle_group_notification, get_group_cache, and notification.actions) so you
remove the per-action awaits and still call group_cache.get(...).await /
group_cache.insert(...).await as before; ensure the moved variable is in scope
for both Add and Remove arms and adjust mutability if needed.
In `@src/retry.rs`:
- Around line 551-567: The registration-ID decoding logic is duplicated across
extract_registration_id_from_node, extract_registration_id_from_node_ref, and
the closure that maps registration_node.and_then(get_bytes_content_ref); extract
that variable-length big-endian parse into a single helper (e.g.,
decode_registration_id_from_bytes or parse_be_u32_varlen) that takes a &[u8] and
returns a u32, then replace the logic in extract_registration_id_from_node,
extract_registration_id_from_node_ref, and the registration_node mapping to call
that helper (use get_bytes_content_ref to obtain the &[u8] before calling the
helper).
In `@wacore/derive/src/lib.rs`:
- Around line 184-270: The generated code currently calls node.attr_parser() for
every attribute access (see match arms that emit expressions using
node.attr_parser()), which is wasteful; change the generator to emit a single
local let parser = node.attr_parser(); at the start of the generated parser body
and then replace all occurrences of node.attr_parser() in the AttrType match
arms with parser so every attribute read (optional_jid, required_string,
optional_string, optional_u64, etc.) uses that single parser instance; ensure
this same change is applied for the other block referenced (lines 329-335) so
both places reuse the local parser variable.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 375-385: The current parsing silently drops malformed <host>
entries because filter_map(... .ok()?) swallows
MediaConnHostExtended::try_from_node_ref errors; change the chain to map each
host_node -> MediaConnHostExtended::try_from_node_ref(host_node) and then map
Ok(ext) into MediaConnHost, collect into a Result<Vec<MediaConnHost>, _> (so use
collect::<Result<_, _>>() or equivalent) and propagate the error with ? so that
parsing fails fast (affecting the hosts construction where
media_conn_node.get_children_by_tag("host") is used and the
MediaConnHostExtended::try_from_node_ref call).
In `@wacore/src/iq/props.rs`:
- Around line 191-216: Refactor try_from_node_ref to dispatch on discriminator
attributes before parsing: inspect the node's distinguishing attribute(s) (e.g.,
the attribute(s) that indicate experiment vs sampling) and if they clearly
indicate AbProp -> call AbProp::try_from_node_ref(node) and return
Self::Experiment on success; if they indicate Sampling -> call
SamplingProp::try_from_node_ref(node) and return Self::Sampling on success; if
both discriminators are present treat as ambiguous and return a clear error, and
if neither present return a "missing discriminator" error. Ensure you remove the
current pattern of invoking both AbProp::try_from_node_ref and
SamplingProp::try_from_node_ref unconditionally so attribute lookups happen only
once and mixed payloads are rejected explicitly.
In `@wacore/src/xml.rs`:
- Around line 67-74: NodeRef::format_attributes currently iterates self.attrs
directly causing nondeterministic attribute ordering; make it consistent with
Node::format_attributes by collecting the keys, sorting them, and then writing
attributes in sorted order (e.g., mirror the sorting logic used in
Node::format_attributes when formatting into out) so both Node and NodeRef
produce deterministic, identical XML output for the same attributes.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 3640-3653: In build_ack_node, avoid stringifying JID-backed
attributes by removing the to_string_cow() round-trip for "from", "participant"
and the "type" branch; instead construct NodeValue directly from the borrowed
attribute/JID returned by node.get_attr(...) (using the NodeValue API overload
that accepts the borrowed/cow or Jid-backed value) so you preserve the original
borrowed/JID representation and eliminate unnecessary allocations when building
ack nodes.
In `@src/client/device_registry.rs`:
- Around line 301-313: The current delete_sessions_for_devices blindly
cross-products lookup.all_keys() with both Server::Lid and Server::Pn, causing
invalid (user,server) pairs to be purged; change delete_sessions_for_devices to
match on each lookup key (from resolve_lookup_keys / UserLookupKeys) and emit
only the actual server(s) for that key (e.g., map Lid->Server::Lid,
Pn->Server::Pn, and for UserLookupKeys::Unknown use the caller-provided server),
then construct Jid with that single server and call
self.signal_cache.delete_session(&addr).await for each device_id — do not
iterate the fixed servers array.
In `@src/handlers/ib.rs`:
- Around line 113-143: Collapse the nested if-let into a single let-chain:
combine the checks for routing_info_node and routing_bytes using `if let
Some(routing_info_node) = child.get_optional_child("routing_info") && let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref() { ... }`, keep the existing logic that
checks for empty bytes and spawns the async task (referencing
client.runtime.spawn, client.persistence_manager.modify_device, and
device.edge_routing_info) inside the true branch, and preserve the two fallback
debug logs in the appropriate else branches (one for missing routing_info child
and one for non-bytes/empty content).
In `@src/handlers/receipt.rs`:
- Line 28: The call currently does an unnecessary Arc refcount bump: replace
client.handle_receipt(Arc::clone(&node)).await with a move of the Arc
(client.handle_receipt(node).await) and update handle_receipt's signature to
accept the Arc by value (e.g., fn handle_receipt(&self, node: Arc<Node>) / async
fn handle_receipt(&self, node: Arc<Node>) as appropriate) so the Arc is moved
instead of cloned; also remove any later uses of `node` in this scope (or
recreate it if needed) so there are no borrow/use-after-move issues.
In `@wacore/src/iq/chatstate.rs`:
- Around line 140-145: The current parsing for `from` in the chatstate parsing
block uses `attrs.optional_jid("from")` which returns None for both missing and
malformed JIDs, so malformed JIDs are being treated as `MissingFrom`/`SelfEcho`;
change the logic around the `let from = match attrs.optional_jid("from") { ...
}` in `chatstate.rs` to detect and map malformed JIDs to
`ChatstateParseError::InvalidJid` instead of collapsing them: either use the
attribute parser API that returns a Result (e.g., `attrs.jid("from")` or
`attrs.require_jid("from")`) so you can match an `Err(InvalidJid)` and return
`ChatstateParseError::InvalidJid`, or, if only `optional_jid` is available,
inspect the parser’s accumulated attribute errors (e.g., `attrs.errors()` or
equivalent) after calling `optional_jid("from")` and return
`ChatstateParseError::InvalidJid` when a malformed- JID error for the "from"
attribute is present; ensure the `SelfEcho`/`MissingFrom` branches remain for
the genuinely missing case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5590e549-0384-4596-a748-75ac203f9412
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (116)
Cargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rs
| .filter_map(|n| { | ||
| let jid = n.attrs().optional_jid("jid")?; | ||
| let key_index = n.attrs().optional_u64("key-index").map(|v| v as u32); | ||
| let jid = n.attr_parser().optional_jid("jid")?; | ||
| let key_index = n.attr_parser().optional_u64("key-index").map(|v| v as u32); | ||
| Some(AccountSyncDevice { jid, key_index }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the specific file and lines mentioned
cat -n src/handlers/notification.rs | sed -n '549,570p'Repository: jlucaso1/whatsapp-rust
Length of output: 1008
🏁 Script executed:
# Also check the broader context to understand parse_account_sync_device_list function
rg -B 10 -A 15 "parse_account_sync_device_list" src/handlers/notification.rsRepository: jlucaso1/whatsapp-rust
Length of output: 6006
🏁 Script executed:
# Search for how key-index is used elsewhere in the codebase
rg "key-index" --type rs -B 2 -A 2Repository: jlucaso1/whatsapp-rust
Length of output: 92
🏁 Script executed:
# Check usync.rs to understand the intended behavior from the retrieved learning
rg -B 5 -A 10 "filter_devices_by_key_index|existing_key_indices" src/usync.rsRepository: jlucaso1/whatsapp-rust
Length of output: 2887
Use u32::try_from(v) to reject overflowing key-index values instead of silently truncating.
The cast v as u32 silently wraps when the server sends a value exceeding u32::MAX. Since key_index is used in Signal protocol key management, a truncated value could cause the client to derive or use the wrong encryption key, leading to device pairing failures or security issues. Replace with u32::try_from(v).ok() to return None for invalid wire data, allowing safe default handling instead of corrupting device state.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/handlers/notification.rs` around lines 557 - 560, The current mapping
silently truncates key-index with `v as u32`, so change the conversion in the
AccountSyncDevice construction (the closure using
attr_parser().optional_u64("key-index")) to use fallible conversion: replace the
cast with a try conversion like u32::try_from(v).ok() so overflowing u64 values
become None and are safely ignored by the surrounding filter_map; keep the rest
of the AccountSyncDevice fields (jid via optional_jid) unchanged.
| let primary_wrapped_ephemeral = match reg_node | ||
| .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"]) | ||
| .and_then(|n| n.content.as_ref()) | ||
| { | ||
| Some(NodeContent::Bytes(b)) if b.len() == 80 => b.clone(), | ||
| _ => { | ||
| .and_then(|n| match n.content.as_deref() { | ||
| Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), | ||
| _ => None, | ||
| }) { | ||
| Some(b) => b, | ||
| None => { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Parse the wrapped ephemeral into a fixed array instead of a Vec.
This branch already validates a hard 80-byte payload, so allocating a Vec here adds avoidable heap traffic on a perf-oriented path. A [u8; 80] keeps the copy stack-backed and still passes cleanly into the blocking decrypt call.
♻️ Proposed change
- let primary_wrapped_ephemeral = match reg_node
+ let primary_wrapped_ephemeral: [u8; 80] = match reg_node
.get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"])
.and_then(|n| match n.content.as_deref() {
- Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()),
+ Some(NodeContentRef::Bytes(b)) => b.as_ref().try_into().ok(),
_ => None,
}) {
Some(b) => b,
None => {
warn!(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pair_code.rs` around lines 238 - 245, The branch that extracts the
wrapped ephemeral currently returns a Vec (primary_wrapped_ephemeral) even
though you already require exactly 80 bytes; change it to produce a fixed-size
array [u8; 80] instead of Vec<u8>. Locate the match on
reg_node.get_optional_child_by_tag(...).and_then(|n| match n.content.as_deref()
{ Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), _ =>
None, }) and replace the Some(b.to_vec()) path with code that converts the slice
into a [u8; 80] (e.g., use try_into() on b and propagate None on failure) so
primary_wrapped_ephemeral becomes a [u8; 80] and can be passed directly to the
downstream blocking decrypt call without heap allocation.
| let (jid, lid) = if let Some(device_node) = success_node.get_optional_child_by_tag(&["device"]) | ||
| { | ||
| let mut parser = device_node.attrs(); | ||
| let mut parser = device_node.attr_parser(); | ||
| let parsed_jid = parser.optional_jid("jid").unwrap_or_default(); | ||
| let parsed_lid = parser.optional_jid("lid").unwrap_or_default(); | ||
|
|
||
| if let Err(e) = parser.finish() { | ||
| warn!(target: "Client/Pair", "Error parsing device node attributes: {e:?}"); | ||
| (Jid::default(), Jid::default()) | ||
| } else { | ||
| (parsed_jid, parsed_lid) | ||
| } | ||
| (parsed_jid, parsed_lid) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure and locate src/pair.rs
git ls-files | grep -E "pair\.rs"Repository: jlucaso1/whatsapp-rust
Length of output: 96
🏁 Script executed:
# Now let's examine the specific lines in src/pair.rs
cat -n src/pair.rs | head -220 | tail -40Repository: jlucaso1/whatsapp-rust
Length of output: 1730
🏁 Script executed:
# Get more context around the pair-success handling
cat -n src/pair.rs | grep -A 10 -B 10 "pair-success"Repository: jlucaso1/whatsapp-rust
Length of output: 3339
🏁 Script executed:
# Search for other uses of optional_jid and unwrap_or_default patterns
rg "optional_jid.*unwrap_or_default" --no-heading -B 2 -A 2Repository: jlucaso1/whatsapp-rust
Length of output: 974
🏁 Script executed:
# Search for how JID validation is done elsewhere in the codebase
rg "optional_jid" -t rs --no-heading -B 1 -A 1 | head -50Repository: jlucaso1/whatsapp-rust
Length of output: 92
🏁 Script executed:
# Look at what happens with the jid and lid after they're parsed
cat -n src/pair.rs | sed -n '200,250p'Repository: jlucaso1/whatsapp-rust
Length of output: 2624
🏁 Script executed:
# Search for where these variables are used in the rest of the function
cat -n src/pair.rs | sed -n '200,320p'Repository: jlucaso1/whatsapp-rust
Length of output: 5767
🏁 Script executed:
# Look for PairSuccess struct or device initialization
rg "struct PairSuccess|PairSuccess\{" -A 5 -B 2Repository: jlucaso1/whatsapp-rust
Length of output: 436
🏁 Script executed:
# Check if jid/lid are validated or checked anywhere
rg "jid.*is_empty|jid.*default|Jid::default" --no-heading -B 2 -A 2 | head -40Repository: jlucaso1/whatsapp-rust
Length of output: 2257
🏁 Script executed:
# Look for the rest of the function to see PairSuccess event dispatch
cat -n src/pair.rs | sed -n '278,350p'Repository: jlucaso1/whatsapp-rust
Length of output: 3028
🏁 Script executed:
# Check how SetId and SetLid commands are handled
rg "DeviceCommand::SetId|DeviceCommand::SetLid" -A 5 -B 2Repository: jlucaso1/whatsapp-rust
Length of output: 1978
🏁 Script executed:
# Check if there's any validation when storing empty JIDs
rg "SetId|SetLid" --no-heading -B 3 -A 3 | grep -A 10 -B 10 "empty\|validate\|default"Repository: jlucaso1/whatsapp-rust
Length of output: 48
Reject pair-success messages with missing or malformed jid/lid attributes instead of defaulting to empty JIDs.
Using optional_jid(...).unwrap_or_default() silently collapses both missing and unparseable jid/lid attributes into Jid::default(). This allows the pairing to succeed and persist empty account identifiers to the device store (via SetId and SetLid commands), creating an inconsistent state. The check at line 263 only prevents LID-PN mapping, not device persistence.
Since device-identity is strictly validated (lines 163–178), jid and lid should be treated with the same rigor: fail the pairing and return an error if either attribute is missing or malformed, rather than proceeding with defaults.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pair.rs` around lines 201 - 206, The current code silently replaces
missing or malformed device jid/lid with Jid::default() by using
optional_jid(...).unwrap_or_default(), which allows invalid pair-success
messages to proceed and persist empty identifiers; change the logic in the
pair-success handling (the block that calls
success_node.get_optional_child_by_tag("device") and uses
device_node.attr_parser() to parse "jid" and "lid") to validate both attributes
strictly: attempt to parse each with optional_jid (or the parser's parsing
method) and if either returns None or an Err, immediately return an error
rejecting the pair-success instead of defaulting, preventing issuing
SetId/SetLid commands with empty JIDs and matching the strict validation used
for device-identity.
| /// Parse LID JID from a `<lid val="..."/>` child node. | ||
| fn parse_lid_jid(user_node: &Node) -> Option<Jid> { | ||
| fn parse_lid_jid(user_node: &NodeRef<'_>) -> Option<Jid> { | ||
| user_node.get_optional_child("lid").and_then(|lid_node| { | ||
| lid_node | ||
| .attrs() | ||
| .attr_parser() | ||
| .optional_string("val") | ||
| .and_then(|val| val.parse::<Jid>().ok()) | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify whether AttrParser distinguishes string attrs from JID attrs,
# then inspect the affected usync call sites.
rg -n -C3 'fn optional_string|fn optional_jid|fn as_str' wacore/binary/src
echo
rg -n -C2 'optional_string\("jid"\)|optional_string\("pn_jid"\)|optional_string\("val"\)' wacore/src/iq/usync.rsRepository: jlucaso1/whatsapp-rust
Length of output: 4657
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the attr_parser() implementation for NodeRef
echo "=== Looking for NodeRef::attr_parser() signature ==="
rg -n 'impl.*NodeRef|fn attr_parser' wacore/binary/src/node.rs -A 2
echo
echo "=== Check ValueRef methods to understand to_string_cow behavior ==="
rg -n 'fn to_string_cow|fn to_jid' wacore/binary/src/node.rs -A 5Repository: jlucaso1/whatsapp-rust
Length of output: 1499
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for tests in usync.rs that test JID parsing
echo "=== Test coverage for usync parsing ==="
rg -n '#\[test\]|fn test' wacore/src/iq/usync.rs | head -20
echo
echo "=== Check if any tests use non-string JID attrs ==="
rg -n 'Jid\(|ValueRef::Jid|NodeValue::Jid' wacore/src/iq/usync.rsRepository: jlucaso1/whatsapp-rust
Length of output: 812
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find AttrParserRef definition and which ValueRef methods it uses
echo "=== AttrParserRef definition ==="
rg -n 'struct AttrParserRef|impl.*AttrParserRef' wacore/binary/src/attrs.rs -A 5
echo
echo "=== Full optional_string and optional_jid for AttrParserRef ==="
rg -n -A 10 'impl.*AttrParserRef' wacore/binary/src/attrs.rs | grep -A 10 'optional_string\|optional_jid'Repository: jlucaso1/whatsapp-rust
Length of output: 491
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search more carefully for AttrParserRef methods
echo "=== AttrParserRef optional_string and optional_jid ==="
sed -n '18,250p' wacore/binary/src/attrs.rs | grep -n -A 5 'fn optional_string\|fn optional_jid'Repository: jlucaso1/whatsapp-rust
Length of output: 1265
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if usync responses actually produce Jid-variant attributes
echo "=== Check how attrs are populated in usync parsing ==="
rg -n 'parse_response|get_optional_child|attr_parser' wacore/src/iq/usync.rs | head -30
echo
echo "=== Look at decoder to understand when Jid variants are created ==="
rg -n 'ValueRef::Jid|NodeValue::Jid' wacore/binary/src/node.rs -B 2 -A 2Repository: jlucaso1/whatsapp-rust
Length of output: 4564
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if there's any code that specifically creates Jid-variant attrs in responses
echo "=== Search for where Jid variants are created during decoding ==="
rg -n 'NodeValue::Jid' wacore/binary/src -B 3 | head -50
echo
echo "=== Check usync test fixtures to see if they ever contain JID attrs ==="
rg -n 'jid|pn_jid|lid' wacore/src/iq/usync.rs -i | grep -E 'test_|#\[test\]|"jid"|"pn_jid"' | head -30Repository: jlucaso1/whatsapp-rust
Length of output: 4932
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look at actual test data to understand what attrs are used
echo "=== Get a test fixture from usync tests ==="
sed -n '855,886p' wacore/src/iq/usync.rsRepository: jlucaso1/whatsapp-rust
Length of output: 1418
Use optional_jid() for cleaner JID attribute parsing.
The current code using optional_string("jid").and_then(|s| s.parse::<Jid>()) works correctly but is inefficient: when the decoder produces a ValueRef::Jid variant (which is supported by the binary protocol), to_string_cow() allocates a formatted string, then the code re-parses it back to Jid. Using optional_jid() directly handles both string and Jid variants without the unnecessary allocation and re-parse cycle.
💡 Suggested fix
fn parse_lid_jid(user_node: &NodeRef<'_>) -> Option<Jid> {
- user_node.get_optional_child("lid").and_then(|lid_node| {
- lid_node
- .attr_parser()
- .optional_string("val")
- .and_then(|val| val.parse::<Jid>().ok())
- })
+ user_node
+ .get_optional_child("lid")
+ .and_then(|lid_node| lid_node.attr_parser().optional_jid("val"))
}
fn parse_user_common_fields(user_node: &NodeRef<'_>) -> Option<ParsedUserFields> {
- let jid = user_node
- .attr_parser()
- .optional_string("jid")?
- .parse::<Jid>()
- .ok()?;
+ let jid = user_node.attr_parser().optional_jid("jid")?;
@@
for user_node in list.get_children_by_tag("user") {
- let Some(jid_str) = user_node.attr_parser().optional_string("jid") else {
+ let Some(jid) = user_node.attr_parser().optional_jid("jid") else {
continue;
};
- let Ok(jid) = jid_str.parse::<Jid>() else {
- continue;
- };
let pn_jid = user_node
.attr_parser()
- .optional_string("pn_jid")
- .and_then(|s| s.parse::<Jid>().ok());
+ .optional_jid("pn_jid");Also applies to: 140-145, 315-326
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/iq/usync.rs` around lines 121 - 128, The current parsing in
parse_lid_jid uses attr_parser().optional_string("val").and_then(|val|
val.parse::<Jid>().ok()) which forces allocation and re-parsing when the decoder
already provides a Jid; replace that pattern with
attr_parser().optional_jid("val") to avoid needless to_string/parse. Apply the
same change to the other occurrences mentioned (the similar parsing blocks
around the parse user/jid logic at the other sites referenced) so they call
optional_jid("...") instead of optional_string(...).and_then(|s|
s.parse::<Jid>().ok()), keeping return types as Option<Jid>.
| /// Builds acknowledgment node for a pairing request from a NodeRef. | ||
| pub fn build_ack_node_ref(request_node: &NodeRef<'_>) -> Option<Node> { | ||
| let to = request_node.get_attr("from").map(|v| v.to_string_cow())?; | ||
| let id = request_node.get_attr("id").and_then(|v| v.as_str())?; | ||
| Some( | ||
| NodeBuilder::new("iq") | ||
| .attrs([ | ||
| ("to", to.to_string()), | ||
| ("id", id.to_string()), | ||
| ("type", "result".to_string()), | ||
| ]) | ||
| .build(), | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Collapse the two ACK builders into one implementation.
build_ack_node_ref now mirrors build_ack_node field-for-field, so the next ACK shape change has to be fixed in two places. Please extract a shared private builder over (to, id) or remove the owned-node variant once the remaining callers are migrated.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/pair.rs` around lines 85 - 98, build_ack_node_ref duplicates
build_ack_node; extract a single private builder that takes (to: &str, id: &str)
and returns the Node, then have both build_ack_node_ref and build_ack_node call
that shared helper. Implement something like a private fn (e.g., build_ack_inner
or build_ack_for) that constructs the Node via NodeBuilder using the provided
&strs, and update build_ack_node_ref to map attrs to &str and pass them through,
and update build_ack_node to forward its owned Strings to the same helper
(converting to &str as needed) so there is a single place to change the ACK
shape.
| fn extract_bytes_ref(node: Option<&NodeRef<'_>>) -> Result<Vec<u8>, anyhow::Error> { | ||
| match node.and_then(|n| n.content.as_deref()) { | ||
| Some(NodeContentRef::Bytes(b)) => Ok(b.to_vec()), | ||
| _ => Err(anyhow::anyhow!("Expected bytes in node content")), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Keep the prekey parser zero-copy through the fixed-size fields.
These branches still to_vec() borrowed bytes and then immediately length-check/copy them into arrays. That reintroduces heap traffic for registration, identity keys, prekeys, and signatures right after the NodeRef migration. Borrow the slice directly and copy into stack arrays only at the final boundary.
Also applies to: 158-172, 241-257, 270-286
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/prekeys.rs` around lines 144 - 147, The parser currently calls
to_vec() on NodeContentRef::Bytes (in extract_bytes_ref and similar branches)
which allocates; instead return a borrowed slice (e.g., Result<&[u8],
anyhow::Error> from extract_bytes_ref or add an extract_bytes_ref_slice that
returns &[u8]) and propagate that borrowed slice through the registration,
identity key, prekey, and signature parsing code paths; only at the final
boundary, when you must produce a fixed-size array, validate the slice length
and copy into the stack array (e.g., using try_into().map_err(...) and copying
into [u8; N]) so no intermediate Vec is allocated. Ensure you update call sites
that expect Vec<u8> to accept &[u8] or perform the single copy at the end in
functions handling registration, identity keys, prekeys, and signatures.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fe06ef599
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[cfg(feature = "serde")] | ||
| impl<'de> serde::Deserialize<'de> for Server { | ||
| fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | ||
| let s = <&str>::deserialize(deserializer)?; |
There was a problem hiding this comment.
Deserialize Server from owned strings
Switching Server deserialization to let s = <&str>::deserialize(...) requires the deserializer to hand out a borrowed string, which many valid serde inputs cannot do (for example streaming/reader-based JSON and several binary formats). In those contexts, deserializing JIDs now fails even when the wire value is a correct server domain. Deserialize into String (or Cow<'de, str>) first, then map with Server::try_from to keep compatibility across serde backends.
Useful? React with 👍 / 👎.
cf3ac5e to
5a19d4b
Compare
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
wacore/src/stanza/business.rs (1)
187-237: 🧹 Nitpick | 🔵 TrivialCollapse these nested option probes into let-chains.
The new
remove/verified_name/subscriptionsparsing branches reintroduce nestedif letblocks in code that was just touched. Flattening them with let-chains will keep the parser easier to scan and aligned with the repo’s expected style.As per coding guidelines, "Use let-chains (
if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks to maintain collapsible if patterns".Also applies to: 302-321
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/stanza/business.rs` around lines 187 - 237, The parsing branches for remove, verified_name (and the similar subscriptions block) use nested if-let blocks; refactor them to use let-chains so the checks are flattened and collapsible (e.g. combine node.get_optional_child("remove") and remove_node.attr_parser().optional_jid("jid") in a single if let chain). Update the branches that produce BusinessNotificationType::RemoveJid / RemoveHash and BusinessNotificationType::VerifiedNameJid / VerifiedNameHash to use single if let chains and call VerifiedName::try_from_node(vn_node)? only after the vn_node is present (or include it in the same chain), and do the same for the profile/subscriptions parsing to replace nested if lets with combined let-chains using get_optional_child, attr_parser().optional_jid / optional_string, and VerifiedName::try_from_node where applicable.src/handlers/ib.rs (1)
123-133: 🛠️ Refactor suggestion | 🟠 MajorRoute edge-routing persistence through a
DeviceCommand.This still mutates
Devicedirectly viamodify_device(...). Please pushedge_routing_infothroughPersistenceManager::process_command(...)instead so the write follows the project's state-mutation path.As per coding guidelines "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 123 - 133, The code currently calls PersistenceManager::modify_device(...) to set device.edge_routing_info directly; instead create or use a DeviceCommand variant (e.g., SetEdgeRoutingInfo or UpdateEdgeRoutingInfo) that carries the routing bytes and send it to PersistenceManager::process_command(...) so mutation flows through the project's state path; locate the async task where client.runtime.spawn is used, stop calling persistence_manager.modify_device, build the DeviceCommand with the routing_bytes (cloned as needed), call persistence_manager.process_command(command).await and read device state via persistence_manager.get_device_snapshot() when reads are required.wacore/src/usync.rs (1)
163-184:⚠️ Potential issue | 🟠 MajorUse
optional_jid()to read JID attributes on the zero-copy path.The zero-copy decoder materializes JID-typed attributes as
ValueRef::Jiddirectly (without string allocation). Usingoptional_string()returnsNonefor Jid variants and silently skips valid PN↔LID mappings. Useoptional_jid()instead, which handles both String and Jid variants.Suggested fix
- let user_jid_str = match user_node.attr_parser().optional_string("jid") { - Some(jid) => jid, + let user_jid = match user_node.attr_parser().optional_jid("jid") { + Some(jid) => jid, None => continue, }; - let user_jid: Jid = match user_jid_str.parse() { - Ok(j) => j, - Err(_) => continue, - }; if user_jid.server != wacore_binary::Server::Pn { continue; } if let Some(lid_node) = user_node.get_optional_child("lid") { - let lid_val = match lid_node.attr_parser().optional_string("val") { - Some(v) => v, - None => continue, - }; - if !lid_val.is_empty() - && let Ok(lid_jid) = lid_val.parse::<Jid>() - && lid_jid.server == wacore_binary::Server::Lid - { + if let Some(lid_jid) = lid_node.attr_parser().optional_jid("val") + && lid_jid.server == wacore_binary::Server::Lid + { mappings.push(UsyncLidMapping { phone_number: user_jid.user.clone(), lid: lid_jid.user.clone(), }); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/usync.rs` around lines 163 - 184, The code currently reads JIDs using user_node.attr_parser().optional_string("jid") and lid_node.attr_parser().optional_string("val"), which fails on zero-copy Jid variants; replace those calls with optional_jid() and handle the returned Jid (or Option<Jid>) directly (e.g., parse/assign to user_jid via user_node.attr_parser().optional_jid(), and for lid use lid_node.attr_parser().optional_jid()) so you don't drop valid PN↔LID mappings; update the matching logic that checks user_jid.server and lid_jid.server to work with the Jid values returned by optional_jid().src/client.rs (1)
2272-2317: 🧹 Nitpick | 🔵 TrivialAvoid re-encoding ACK stanzas just to satisfy the waiter type.
handle_ack_response()marshals a borrowedNodeRefback into bytes and immediately reparses it intoOwnedNodeRef. That puts an extra encode/decode + allocation cycle on every ack-waited send and gives back some of the benefit of the new zero-copy path. If the caller already hasArc<OwnedNodeRef>, thread that through here and forward it directly.♻️ Proposed direction
-pub(crate) async fn handle_ack_response(&self, node: &wacore_binary::NodeRef<'_>) -> bool { +pub(crate) async fn handle_ack_response( + &self, + node: Arc<wacore_binary::OwnedNodeRef>, +) -> bool { + let nr = node.get(); - let id_opt = node.get_attr("id").map(|v| v.to_string_cow().into_owned()); + let id_opt = nr.get_attr("id").and_then(|v| v.as_str()); if let Some(id) = id_opt && let Some(waiter) = self.response_waiters.lock().await.remove(id) { - match wacore_binary::marshal::marshal_ref(node) - .and_then(|bytes| wacore_binary::OwnedNodeRef::new(bytes[1..].to_vec())) - { - Ok(onr) => { - if waiter.send(Arc::new(onr)).is_err() { - warn!(target: "Client/Ack", "Failed to send ACK response to waiter for ID {id}. Receiver was likely dropped."); - } - } - Err(e) => { - warn!(target: "Client/Ack", "Failed to re-encode ACK node for waiter: {e}"); - } + if waiter.send(node).is_err() { + warn!(target: "Client/Ack", "Failed to send ACK response to waiter for ID {id}. Receiver was likely dropped."); } return true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 2272 - 2317, handle_ack_response currently re-encodes the borrowed NodeRef into bytes then reconstructs an OwnedNodeRef before sending to the waiter, causing extra encode/decode and allocations; change the waiter plumbing so this function forwards an existing Arc<OwnedNodeRef> when available instead of re-marshaling: update the type stored in self.response_waiters (and the code that inserts waiters) to accept Arc<OwnedNodeRef> or a enum that can carry either Arc<OwnedNodeRef> or a NodeRef borrow, then in handle_ack_response, when you find the waiter directly send the existing Arc<OwnedNodeRef> (via waiter.send(arc_onr)) and remove the marshal::marshal_ref/OwnedNodeRef::new re-encode path and related error handling; keep fallbacks only if no Arc is present.wacore/src/iq/mediaconn.rs (1)
359-385:⚠️ Potential issue | 🟠 MajorDo not turn malformed
media_connresponses into partial data.This parser currently swallows two classes of protocol errors: invalid numeric attrs are ignored because
attrs.finish()is never called, and malformed<host>children are dropped byfilter_map(...ok()?). That can leave callers withttl = 0and a truncated host list instead of a hard parse failure.🐛 Suggested fix
let mut attrs = media_conn_node.attr_parser(); let auth = attrs .optional_string("auth") .ok_or_else(|| anyhow!("Missing 'auth' attribute in media_conn response"))? .to_string(); let ttl = attrs.optional_u64("ttl").unwrap_or(0); let auth_ttl = attrs.optional_u64("auth_ttl"); let max_buckets = attrs.optional_u64("max_buckets"); + attrs.finish()?; // Parse extended host info (type, fallback) and map to MediaConnHost. // Sort: primary hosts first, fallback hosts second (matches WA Web's mapParsedMediaConn). - let mut hosts: Vec<MediaConnHost> = media_conn_node + let mut hosts: Vec<MediaConnHost> = media_conn_node .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) + .map(MediaConnHostExtended::try_from_node_ref) + .collect::<Result<Vec<_>, _>>()? + .into_iter() + .map(|ext| MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) .collect();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 359 - 385, In parse_response, don't silently drop malformed fields: call attrs.finish() after reading auth/ttl/auth_ttl/max_buckets to surface invalid numeric attrs, and change the host parsing pipeline that currently uses filter_map(MediaConnHostExtended::try_from_node_ref.ok()?) to a fallible parse that returns Err if any host fails (e.g., map each host_node with MediaConnHostExtended::try_from_node_ref and propagate errors instead of filtering them out), then build Vec<MediaConnHost> from the successful conversions; this ensures parse_response returns an error on malformed numeric attributes or malformed <host> children rather than producing partial data.wacore/src/iq/props.rs (1)
191-216: 🧹 Nitpick | 🔵 TrivialAvoid parsing each
<prop>twice.
AbPropConfig::try_from_node_refcurrently runs both parsers and builds both error paths before deciding which variant it is. Dispatching once onconfig_codevsevent_codewill cut duplicate attribute scans and error construction from this response parser.♻️ Suggested shape
fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self, anyhow::Error> { if node.tag != "prop" { return Err(anyhow::anyhow!("expected <prop>, got <{}>", node.tag)); } - let experiment = AbProp::try_from_node_ref(node); - if let Ok(prop) = experiment { - return Ok(Self::Experiment(prop)); - } - - let sampling = SamplingProp::try_from_node_ref(node); - if let Ok(prop) = sampling { - return Ok(Self::Sampling(prop)); - } - - let experiment_err = experiment - .err() - .unwrap_or_else(|| anyhow::anyhow!("unknown error")); - let sampling_err = sampling - .err() - .unwrap_or_else(|| anyhow::anyhow!("unknown error")); - Err(anyhow::anyhow!( - "prop did not match experiment or sampling config: experiment_err={}; sampling_err={}", - experiment_err, - sampling_err - )) + match ( + node.get_attr("config_code").is_some(), + node.get_attr("event_code").is_some(), + ) { + (true, false) => AbProp::try_from_node_ref(node).map(Self::Experiment), + (false, true) => SamplingProp::try_from_node_ref(node).map(Self::Sampling), + _ => Err(anyhow::anyhow!( + "prop must contain exactly one of config_code or event_code" + )), + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/props.rs` around lines 191 - 216, The current try_from_node_ref implementation needlessly invokes both AbProp::try_from_node_ref and SamplingProp::try_from_node_ref and constructs both error paths; instead, read the distinguishing attribute(s) on the NodeRef (e.g., look for "config_code" vs "event_code" or whatever attribute identifies an Experiment vs Sampling) and dispatch once to the appropriate parser (call AbProp::try_from_node_ref only when the node indicates an Experiment, SamplingProp::try_from_node_ref only when it indicates Sampling). If the attribute is missing or unrecognized, return a clear error; if the chosen parser fails, return that parser's error directly. Keep references to try_from_node_ref, AbProp, SamplingProp, and NodeRef to locate and change the code.
♻️ Duplicate comments (23)
src/test_utils.rs (1)
8-10:⚠️ Potential issue | 🟡 MinorAvoid the extra copy and guard the format-byte assumption.
Line 10 allocates a second buffer and can panic on empty output. Strip the leading format byte in-place and assert expectations explicitly.
♻️ Proposed fix
pub fn node_to_owned_ref(node: &Node) -> Arc<OwnedNodeRef> { - let bytes = wacore_binary::marshal::marshal(node).expect("marshal should succeed"); + let mut bytes = wacore_binary::marshal::marshal(node).expect("marshal should succeed"); // marshal() prepends a leading format byte; OwnedNodeRef::new expects raw protocol bytes - Arc::new(OwnedNodeRef::new(bytes[1..].to_vec()).expect("OwnedNodeRef::new should succeed")) + assert!( + bytes.first() == Some(&0x00), + "marshal payload must start with format byte 0x00" + ); + bytes.remove(0); + Arc::new(OwnedNodeRef::new(bytes).expect("OwnedNodeRef::new should succeed")) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test_utils.rs` around lines 8 - 10, The code currently creates a second buffer by slicing bytes[1..].to_vec() and can panic on empty output; instead, after calling wacore_binary::marshal::marshal(node) into bytes, assert bytes is non-empty and that bytes[0] matches the expected format byte, then remove the first byte in-place (so the existing Vec is reused) before calling OwnedNodeRef::new; update the call sites (the bytes variable, marshal(), and OwnedNodeRef::new usage inside Arc::new) to perform these checks and the in-place strip to avoid the extra allocation and to guard the format-byte assumption.wacore/src/iq/business.rs (1)
55-59: 🧹 Nitpick | 🔵 TrivialAvoid the extra allocation in byte-to-string conversion.
Line [58] clones bytes into a
Vec<u8>before UTF-8 validation. Validate borrowed bytes first, then allocate once for the finalString.♻️ Proposed refactor
fn node_text(node: &NodeRef<'_>) -> Option<String> { match node.content.as_deref() { Some(NodeContentRef::String(s)) => Some(s.to_string()), - Some(NodeContentRef::Bytes(b)) => String::from_utf8(b.to_vec()).ok(), + Some(NodeContentRef::Bytes(b)) => std::str::from_utf8(b).ok().map(str::to_owned), _ => None, } }#!/bin/bash # Verify whether the extra-allocation pattern still exists in the file. rg -n 'String::from_utf8\(b\.to_vec\(\)\)\.ok\(\)' wacore/src/iq/business.rs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/business.rs` around lines 55 - 59, In node_text, avoid cloning the bytes with b.to_vec() before UTF-8 validation; instead validate the borrowed bytes (NodeContentRef::Bytes(b)) with std::str::from_utf8(b) and then allocate a String only once (e.g., map the &str to String) so you replace String::from_utf8(b.to_vec()).ok() with a from_utf8-based path that returns Some(s.to_string()) on success; update the match arm in fn node_text accordingly.wacore/appstate/src/patch_decode.rs (1)
89-99:⚠️ Potential issue | 🟠 Major
*_refentry points still allocate and negate zero-copy parsing.
parse_patch_list_refandparse_patch_lists_refconvert&NodeRef<'_>to ownedNodeviato_owned(), which defeats the zero-copy path introduced by this PR.♻️ Suggested direction
pub fn parse_patch_list_ref(node: &NodeRef<'_>) -> Result<PatchList> { - parse_patch_list(&node.to_owned()) + let collection = node + .get_optional_child_by_tag(&["sync", "collection"]) + .ok_or_else(|| anyhow!("missing sync/collection"))?; + parse_single_collection_ref(&collection) } pub fn parse_patch_lists_ref(node: &NodeRef<'_>) -> Result<Vec<PatchList>> { - parse_patch_lists(&node.to_owned()) + let sync_node = if node.tag == "sync" { + node + } else { + node.get_optional_child("sync") + .ok_or_else(|| anyhow!("missing sync node in response"))? + }; + parse_collections_ref(sync_node) }Then mirror
parse_single_collection/collection iteration on borrowedNodeRefhelpers to remove full-tree cloning from this path.#!/bin/bash set -euo pipefail # Verify current zero-copy wrappers still clone. rg -n "parse_patch_list_ref|parse_patch_lists_ref|to_owned\\(" wacore/appstate/src/patch_decode.rs # Verify whether borrowed helper(s) exist yet. rg -n "parse_single_collection_ref|parse_collections_ref|parse_patch_list_ref|parse_patch_lists_ref" wacore/appstate/src/patch_decode.rs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/appstate/src/patch_decode.rs` around lines 89 - 99, parse_patch_list_ref and parse_patch_lists_ref currently call to_owned() and thus clone the whole Node, defeating the zero-copy intent; instead add or reuse borrowed helpers that operate on &NodeRef to parse in-place (mirror the existing parse_single_collection/collection-iteration logic but for borrowed NodeRef), then have parse_patch_list_ref call that borrowed parse_single_collection_ref (or implement parse_collections_ref) and parse_patch_lists_ref iterate child <collection> elements via a borrowed helper without calling to_owned(); reference parse_patch_list, parse_patch_lists, parse_single_collection and implement corresponding parse_patch_list_ref/parse_patch_lists_ref helpers that walk NodeRef children zero-copy.wacore/src/pair_code.rs (1)
32-34:⚠️ Potential issue | 🟡 MinorPair-code IQ builders still use
SERVER_JIDinstead ofServer::Pn.This module remains on string-based server targeting while the rest of the migration moved to the typed
Serverenum.♻️ Proposed fix
-use wacore_binary::SERVER_JID; +use wacore_binary::Server; @@ - ("to", SERVER_JID.to_string()), + ("to", Server::Pn.as_str().to_string()), @@ - ("to", SERVER_JID.to_string()), + ("to", Server::Pn.as_str().to_string()),#!/bin/bash set -euo pipefail # Check remaining legacy server constant usage in this file and related modules. rg -n "\\bSERVER_JID\\b|\\bServer::Pn\\b" wacore/src/pair_code.rs rg -n "\\bSERVER_JID\\b" wacore/src/iqAlso applies to: 336-337, 383-384
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/pair_code.rs` around lines 32 - 34, The pair_code module still imports and uses the string constant SERVER_JID; replace all uses with the typed Server enum variant Server::Pn and update imports accordingly (remove wacore_binary::SERVER_JID and add use wacore_binary::Server). In call sites/builders (e.g., any functions that pass SERVER_JID into NodeBuilder/Node/NodeRef/NodeContentRef), convert to the proper representation expected by those APIs (e.g., Server::Pn or Server::Pn.to_jid() / Server::Pn.as_str() as required by NodeBuilder) and run the ripgrep suggested checks to update the other occurrences in the iq modules as well. Ensure compilation by adjusting types where SERVER_JID string was assumed.wacore/src/iq/tctoken.rs (1)
281-287: 🧹 Nitpick | 🔵 TrivialAvoid formatting the decoded JID only to parse it again.
get_attr("jid")has already decoded this attribute, soto_string_cow().into_owned().parse()adds an avoidable allocation and a second parse in the response path.Minimal diff
- let jid_str = token_node - .get_attr("jid") - .map(|v| v.to_string_cow().into_owned()) - .ok_or_else(|| anyhow::anyhow!("missing required attribute jid"))?; - let jid: Jid = jid_str - .parse() - .map_err(|e| anyhow::anyhow!("invalid jid '{}': {}", jid_str, e))?; + let jid = token_node + .get_attr("jid") + .and_then(|v| v.to_jid()) + .ok_or_else(|| anyhow::anyhow!("missing or invalid required attribute jid"))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/tctoken.rs` around lines 281 - 287, The code currently forces an owned string and reparses the JID; instead use the already-decoded attribute value without an extra allocation or reformat/parse. Replace the jid_str construction and subsequent .parse() call so you take the attribute's borrowed string (from token_node.get_attr("jid") e.g. v.as_ref() or similar) and pass that borrowed &str directly into Jid parsing/constructor (or use a conversion that accepts &str) and keep the same error handling (ok_or_else for missing attribute and map_err for invalid JID). Reference: token_node.get_attr("jid"), jid_str, and Jid.src/handlers/ib.rs (1)
113-116: 🛠️ Refactor suggestion | 🟠 MajorCollapse the
routing_infoextraction into a let-chain.This branch still uses nested
if letblocks. Please fold them into a single let-chain so it matches the repository pattern and keeps the fallback logging branches collapsible.Suggested change
- if let Some(routing_info_node) = child.get_optional_child("routing_info") { - if let Some(NodeContentRef::Bytes(routing_bytes)) = - routing_info_node.content.as_deref() - { + let routing_info_node = child.get_optional_child("routing_info"); + if let Some(routing_info_node) = routing_info_node.as_ref() + && let Some(NodeContentRef::Bytes(routing_bytes)) = + routing_info_node.content.as_deref() + { if !routing_bytes.is_empty() { debug!( "Received edge routing info ({} bytes), storing for reconnection", @@ } else { debug!("Received empty edge routing info, ignoring"); } - } else { - debug!("Edge routing info node has no bytes content"); - } - } else { - debug!("Edge routing stanza has no routing_info child"); - } + } else if routing_info_node.is_none() { + debug!("Edge routing stanza has no routing_info child"); + } else { + debug!("Edge routing info node has no bytes content"); + }As per coding guidelines "Use let-chains (
if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks to maintain collapsible if patterns".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 113 - 116, Collapse the nested `if let` into a let-chain: replace the two-level check that first binds `routing_info_node` from `child.get_optional_child("routing_info")` and then matches `routing_info_node.content.as_deref()` to `Some(NodeContentRef::Bytes(routing_bytes))` with a single `if let Some(routing_info_node) = child.get_optional_child("routing_info") && let Some(NodeContentRef::Bytes(routing_bytes)) = routing_info_node.content.as_deref()` pattern so the branch is one collapsible `if` block; keep the existing fallback logging branches unchanged and ensure `routing_bytes` is the bound identifier used inside the body.src/client/device_registry.rs (1)
301-313:⚠️ Potential issue | 🟠 MajorPreserve the real
(user, server)pairs when purging sessions.This still cross-products
lookup.all_keys()with bothServer::LidandServer::Pn, so a mapped user gets four session deletions, including invalid mixed pairs. That can purge an unrelated session if a numeric LID collides with a real PN, and it also means theUnknownpath still can't honor the caller's actual server. Match onUserLookupKeysand only delete the concrete pairs you resolved.Suggested direction
match lookup { UserLookupKeys::LidWithPn { lid, pn } | UserLookupKeys::PnWithLid { lid, pn } => { purge(&lid, wacore_binary::Server::Lid, device_ids).await; purge(&pn, wacore_binary::Server::Pn, device_ids).await; } UserLookupKeys::Unknown { user } => { purge(&user, caller_server, device_ids).await; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/device_registry.rs` around lines 301 - 313, The current delete_sessions_for_devices cross-products resolve_lookup_keys() with both Server::Lid and Server::Pn, which can purge incorrect pairs; change delete_sessions_for_devices(&self, user: &str, device_ids: &[u16]) to accept the caller's server (e.g. caller_server: wacore_binary::Server), call resolve_lookup_keys(user).await and match on the returned UserLookupKeys variants (UserLookupKeys::LidWithPn { lid, pn }, UserLookupKeys::PnWithLid { lid, pn }, UserLookupKeys::Unknown { user }) and only construct Jid::new(...) with the concrete (key, server) pairs for each variant (use Server::Lid for lid and Server::Pn for pn; for Unknown use caller_server), then call signal_cache.delete_session with the protocol address for each device_id — do not iterate over both servers for every key.wacore/src/prekeys.rs (1)
144-147: 🛠️ Refactor suggestion | 🟠 MajorAvoid reintroducing heap copies in the zero-copy prekey parser.
These paths still call
to_vec()on borrowedNodeContentRef::Bytesand then immediately length-check/copy into fixed-size arrays. That gives back a chunk of the allocation savings from theNodeRefmigration on the prekey parse path. Keep these helpers on&[u8], validate lengths on the borrowed slice, and only copy once at the final[u8; N]boundary.Also applies to: 158-175, 241-257, 270-286
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/prekeys.rs` around lines 144 - 147, The helper extract_bytes_ref currently calls to_vec() on NodeContentRef::Bytes which reintroduces heap allocation; change it to return Result<&[u8], anyhow::Error> (keep the borrowed slice) and perform length validation on that &[u8] where called, only copying into fixed-size arrays at the final conversion point (e.g., when constructing [u8; N] in the prekey parsing functions). Apply the same change to the other helper usages in the file (the blocks around the commented ranges 158-175, 241-257, 270-286): avoid to_vec(), validate slice length via slice.len() or try_into() on the borrowed &[u8], and perform a single copy into the fixed-size array only when the length is correct.src/pair.rs (2)
54-56: 🧹 Nitpick | 🔵 TrivialKeep QR ref decoding borrowed until UTF-8 validation succeeds.
String::from_utf8(bytes.to_vec())allocates before you know the payload is valid UTF-8. Borrowed validation keeps the zero-copy path intact and only allocates for the final owned string.♻️ Minimal diff
- if let Some(NodeContentRef::Bytes(bytes)) = grandchild.content.as_deref() - && let Ok(r) = String::from_utf8(bytes.to_vec()) + if let Some(NodeContentRef::Bytes(bytes)) = grandchild.content.as_deref() + && let Ok(r) = std::str::from_utf8(bytes.as_ref()) { - codes.push(PairUtils::make_qr_data(&device_state, r)); + codes.push(PairUtils::make_qr_data(&device_state, r.to_owned())); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair.rs` around lines 54 - 56, The code prematurely allocates by calling String::from_utf8(bytes.to_vec()) while still holding a borrowed reference from grandchild.content.as_deref(); instead validate the borrowed bytes first using std::str::from_utf8(bytes) (or core::str::from_utf8) to keep the zero-copy path, and only after the from_utf8() succeeds convert to an owned String (e.g., .to_owned() or String::from) for further use; locate the NodeContentRef::Bytes match and replace the String::from_utf8(bytes.to_vec()) check with a borrowed UTF-8 validation using from_utf8, then map the validated &str to an owned String.
201-208:⚠️ Potential issue | 🟠 MajorReject pair-success stanzas with missing or malformed
jid/lid.
unwrap_or_default()collapses bad data intoJid::default(), and the success path later persists those values throughSetId/SetLid. That can leave the device store in an inconsistent paired state after a malformed stanza.🛠️ Minimal diff
- let (jid, lid) = if let Some(device_node) = success_node.get_optional_child_by_tag(&["device"]) - { - let mut parser = device_node.attr_parser(); - let parsed_jid = parser.optional_jid("jid").unwrap_or_default(); - let parsed_lid = parser.optional_jid("lid").unwrap_or_default(); - (parsed_jid, parsed_lid) - } else { - (Jid::default(), Jid::default()) - }; + let Some(device_node) = success_node.get_optional_child_by_tag(&["device"]) else { + error!("pair-success is missing <device>"); + return; + }; + let mut parser = device_node.attr_parser(); + let (Some(jid), Some(lid)) = (parser.optional_jid("jid"), parser.optional_jid("lid")) else { + error!("pair-success is missing jid/lid attributes"); + return; + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair.rs` around lines 201 - 208, The code currently uses unwrap_or_default() for parsed_jid/parsed_lid which masks missing or malformed values; instead, change the logic in the success_node/device_node branch to explicitly validate both parser.optional_jid("jid") and parser.optional_jid("lid") and reject the pair-success stanza if either is missing or fails to parse (do not substitute Jid::default()). Locate the parsing in success_node -> device_node where parsed_jid/parsed_lid are extracted and replace the unwrap_or_default usage with an early error/reject path (e.g., return Err or call the existing stanza-reject handler) so invalid jid/lid values are not persisted later via SetId/SetLid.wacore/src/iq/usync.rs (1)
121-128: 🧹 Nitpick | 🔵 TrivialParse JID-typed attrs with
optional_jid()directly.These paths still stringify JID attrs and parse them back into
Jid. On the newNodeRefpath that drops the already-decoded typed value and adds avoidable allocation/reparse work in hot usync parsing.♻️ Representative diff
fn parse_lid_jid(user_node: &NodeRef<'_>) -> Option<Jid> { - user_node.get_optional_child("lid").and_then(|lid_node| { - lid_node - .attr_parser() - .optional_string("val") - .and_then(|val| val.parse::<Jid>().ok()) - }) + user_node + .get_optional_child("lid") + .and_then(|lid_node| lid_node.attr_parser().optional_jid("val")) } fn parse_user_common_fields(user_node: &NodeRef<'_>) -> Option<ParsedUserFields> { - let jid = user_node - .attr_parser() - .optional_string("jid")? - .parse::<Jid>() - .ok()?; + let jid = user_node.attr_parser().optional_jid("jid")?;Also applies to: 140-145, 315-326, 558-564
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/usync.rs` around lines 121 - 128, Replace the current pattern that reads a JID attr as a string then reparses it with the typed accessor: in parse_lid_jid (and the other similar parsing sites) swap user_node.get_optional_child("lid").and_then(|lid_node| lid_node.attr_parser().optional_string("val").and_then(|val| val.parse::<Jid>().ok())) for user_node.get_optional_child("lid").and_then(|lid_node| lid_node.attr_parser().optional_jid("val")), preserving the Option<Jid> return type; apply the same change to the other occurrences that currently stringify then parse JID attributes.src/pair_code.rs (1)
238-245: 🧹 Nitpick | 🔵 TrivialParse the wrapped ephemeral into a fixed array instead of a
Vec.This branch already requires exactly 80 bytes, so keeping it as a
Vec<u8>adds an avoidable allocation right beforedecrypt_primary_ephemeral_pub.♻️ Minimal diff
- let primary_wrapped_ephemeral = match reg_node + let primary_wrapped_ephemeral: [u8; 80] = match reg_node .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"]) .and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), + Some(NodeContentRef::Bytes(b)) => b.as_ref().try_into().ok(), _ => None, }) { Some(b) => b, None => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair_code.rs` around lines 238 - 245, The code currently extracts an 80-byte NodeContentRef::Bytes into a Vec<u8> (primary_wrapped_ephemeral) even though the length is enforced; change the extraction to produce a fixed array [u8; 80] instead. In the match inside reg_node.get_optional_child_by_tag(...) when you see Some(NodeContentRef::Bytes(b)) if b.len() == 80, convert b into a [u8;80] (e.g. use b.try_into().ok() or allocate let mut arr = [0u8;80]; arr.copy_from_slice(&b)) and return that array (or a reference) instead of Vec<u8>, then pass the [u8;80] (or &arr) into decrypt_primary_ephemeral_pub so the extra heap allocation is eliminated; update any bindings/types that expect Vec<u8> accordingly.wacore/binary/src/jid.rs (1)
508-512:⚠️ Potential issue | 🟠 Major
actual_agent()still leaks agent state on other agent-less servers.Only zeroing
Server::Pnis inconsistent with the rest of this file, which also treatsServer::Lid,Server::Hosted, andServer::HostedLidas agent-less encodings. Manually constructed JIDs can therefore retain agent bytes that disappear when formatted and reparsed.Minimal fix
pub fn actual_agent(&self) -> u8 { match self.server { - Server::Pn => 0, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid => 0, _ => self.agent, } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 508 - 512, The actual_agent() method currently only zeros the agent for Server::Pn but should treat all agent-less server variants consistently; update actual_agent() (in the JID implementation) to return 0 when self.server is any of Server::Pn, Server::Lid, Server::Hosted, or Server::HostedLid and otherwise return self.agent so manually constructed JIDs don't leak leftover agent bytes.wacore/src/iq/blocklist.rs (1)
116-131: 🧹 Nitpick | 🔵 TrivialReuse
BlocklistResponse::try_from_node_refhere.This reimplements the same
<list>/direct-<item>traversal and warning path thatBlocklistResponsealready owns, so future schema changes now need two fixes.Minimal cleanup
fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { - // BlocklistResponse checks for a <list> child or direct <item> children - let entries = if let Some(list) = response.get_optional_child("list") { - list.get_children_by_tag("item") - } else { - response.get_children_by_tag("item") - } - .filter_map(|item| match BlocklistEntry::try_from_node_ref(item) { - Ok(entry) => Some(entry), - Err(e) => { - warn!(target: "blocklist", "Failed to parse blocklist entry: {e}"); - None - } - }) - .collect(); - Ok(entries) + Ok(BlocklistResponse::try_from_node_ref(response)?.entries) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/blocklist.rs` around lines 116 - 131, The parse_response implementation duplicates BlocklistResponse's traversal and warning logic; replace the manual <list>/<item> parsing in parse_response with a direct call to BlocklistResponse::try_from_node_ref(response) (or the appropriate BlocklistResponse conversion helper) and return its result, removing the duplicated filter_map/warn logic so future schema changes remain centralized in BlocklistResponse::try_from_node_ref.wacore/src/iq/chatstate.rs (1)
140-153:⚠️ Potential issue | 🟠 MajorMalformed
from/participantJIDs are still treated as missing.
optional_jid()returnsNoneon parse failure, so a badfromstill becomesMissingFrom, and a badparticipantdowngrades a group chatstate into a user chatstate instead of failing withInvalidJid. This parser needs to distinguish malformed from absent.#!/bin/bash set -euo pipefail printf '=== chatstate parser ===\n' rg -n 'pub fn parse\(node: &NodeRef' wacore/src/iq/chatstate.rs -A25 -B5 printf '\n=== optional_jid behavior ===\n' rg -n 'fn optional_jid|errors' wacore/binary/src -A8 -B4Expected result:
optional_jidrecords parse failures internally and returnsNone, while the chatstate parser never checks that error state before treating the attribute as absent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/chatstate.rs` around lines 140 - 153, The parser currently treats malformed JIDs as absent because optional_jid("from") and optional_jid("participant") return None on parse failure; update the pub fn parse(...) in chatstate.rs to distinguish absent vs malformed by checking the attribute parser's parse-error state after each optional_jid call and returning ChatstateParseError::InvalidJid when a parse failure occurred (instead of mapping to MissingFrom or silently falling back to a user chatstate); specifically, after calling attrs.optional_jid("from") handle None by querying the attrs error state for the "from" attribute and return InvalidJid (or SelfEcho if the existing logic for "to" applies), and likewise when handling attrs.optional_jid("participant") make malformed participant JIDs produce ChatstateParseError::InvalidJid rather than treating the node as a non-group chatstate (so construct ChatstateSource::Group only when participant parsed successfully).src/handlers/notification.rs (1)
557-560:⚠️ Potential issue | 🟠 MajorReject overflowing
key-indexvalues instead of truncating them.
v as u32silently wraps large wire values. That can corrupt the stored key-index for a device and send later key-management logic down the wrong path. Use a fallible conversion and drop invalid values instead of narrowing them unchecked.Minimal fix
- let key_index = n.attr_parser().optional_u64("key-index").map(|v| v as u32); + let key_index = n + .attr_parser() + .optional_u64("key-index") + .and_then(|v| u32::try_from(v).ok());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/notification.rs` around lines 557 - 560, The closure in filter_map currently truncates large key-index values with `v as u32`; replace that with a fallible conversion using `u32::try_from` and, if the conversion fails, drop the entire item (return None from the closure) instead of silently truncating. Concretely, change the `key_index` handling in the closure that builds AccountSyncDevice so that `optional_u64("key-index")` is matched: if Some(v) then attempt `u32::try_from(v)` and return None on Err, otherwise use the Ok(u32) value (or keep None if the attribute is absent); keep the rest of the closure building `AccountSyncDevice { jid, key_index }`.src/client.rs (1)
3640-3653: 🧹 Nitpick | 🔵 TrivialPreserve typed attrs when building ACKs.
These
to_string_cow()calls force JID-backed attrs back through string formatting before reconstructingNodeValue, so the ACK path still pays avoidable formatting/allocation costs and loses the typed encoding introduced by this PR. BuildNodeValuedirectly fromValueRefinstead.♻️ Proposed fix
fn build_ack_node(node: &wacore_binary::NodeRef<'_>, own_device_pn: Option<&Jid>) -> Option<Node> { - let id = NodeValue::from(node.get_attr("id")?.to_string_cow().as_ref()); - let from = NodeValue::from(node.get_attr("from")?.to_string_cow().as_ref()); - let participant = node - .get_attr("participant") - .map(|v| NodeValue::from(v.to_string_cow().as_ref())); + let to_node_value = |value: &wacore_binary::ValueRef<'_>| match value { + wacore_binary::ValueRef::String(s) => NodeValue::from(s.as_ref()), + wacore_binary::ValueRef::Jid(j) => NodeValue::Jid(j.to_owned()), + }; + + let id = to_node_value(node.get_attr("id")?); + let from = to_node_value(node.get_attr("from")?); + let participant = node.get_attr("participant").map(to_node_value); @@ let typ = if tag != "message" && !is_encrypt_identity_notification(node) { - node.get_attr("type") - .map(|v| NodeValue::from(v.to_string_cow().as_ref())) + node.get_attr("type").map(to_node_value) } else { None };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3640 - 3653, The build_ack_node function is rebuilding NodeValue from strings by calling to_string_cow() on attributes (losing typed JID encoding and forcing allocations); update the construction to build NodeValue directly from the ValueRef returned by node.get_attr(...) (e.g., pass the ValueRef into NodeValue constructors instead of calling to_string_cow().as_ref()), so id, from, participant, and any other attr NodeValue creations preserve typed encodings and avoid unnecessary formatting/allocation.wacore/src/iq/node.rs (1)
21-25: 🧹 Nitpick | 🔵 Trivial
required_attrstill eagerly allocates on the zero-copy path.This helper is now the choke point for required string attrs, so
to_string()gives back one allocation per read. ReturnCow<'_, str>here, likeoptional_attr, and let callers opt into ownership only when they actually need it.♻️ Minimal refactor
pub(crate) fn required_attr(node: &NodeRef<'_>, key: &str) -> Result<String, anyhow::Error> { - node.get_attr(key) - .map(|v| v.to_string()) - .ok_or_else(|| anyhow!("missing required attribute {key}")) + node.attr_parser() + .optional_string(key) + .map(|v| v.into_owned()) + .ok_or_else(|| anyhow!("missing required attribute {key}")) }-pub(crate) fn required_attr(node: &NodeRef<'_>, key: &str) -> Result<String, anyhow::Error> { +pub(crate) fn required_attr<'a>( + node: &'a NodeRef<'_>, + key: &str, +) -> Result<Cow<'a, str>, anyhow::Error> { node.attr_parser() .optional_string(key) - .map(|v| v.into_owned()) .ok_or_else(|| anyhow!("missing required attribute {key}")) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/node.rs` around lines 21 - 25, required_attr currently forces an allocation by calling to_string() on NodeRef::get_attr results; change its signature to return Result<Cow<'_, str>, anyhow::Error> (matching optional_attr), map the Option<&str> to Cow::from (or .map(Cow::from)) instead of to_string(), and keep the same missing-attribute error via ok_or_else(|| anyhow!("missing required attribute {key}")); update callers that need owned Strings to call .into_owned() on the returned Cow.wacore/src/appstate_sync.rs (1)
98-156: 🛠️ Refactor suggestion | 🟠 MajorThe single-patch owned/ref paths still duplicate hydration logic.
decode_patch_list_refanddecode_patch_liststill repeat the same snapshot/external-mutations download-and-replace flow. Multi-patch now hasprocess_patch_lists, so leaving the single-patch path duplicated is still a drift risk for logging and future protocol changes.Also applies to: 158-216
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/appstate_sync.rs` around lines 98 - 156, decode_patch_list_ref duplicates the snapshot+external-mutations download-and-replace logic that also exists in decode_patch_list; extract that shared hydration flow into a single helper (e.g. hydrate_external_references or similar) that takes a PatchList/vec of patches and the download Fn and performs the snapshot and external_mutations replacement and logging, then call that helper from both decode_patch_list_ref and decode_patch_list (or route the single-patch path through the existing process_patch_lists flow) and keep calls to process_patch_list/process_patch_lists unchanged so logging and behavior remain consistent across single- and multi-patch paths.src/message.rs (1)
366-383: 🛠️ Refactor suggestion | 🟠 MajorCustom enc handlers still punch a hole in the zero-copy path.
(*enc_node).to_owned()re-materializes the full<enc>subtree before dispatch, so any extension handler pays the old allocation cost again. Since this PR is already changing handler-facing APIs elsewhere, I’d still move the custom handler API to&NodeRef/&OwnedNodeRefand clone only in handlers that truly need ownership.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 366 - 383, The custom enc handler dispatch currently forces re-materialization via (*enc_node).to_owned(); update the custom handler API and call-site to take a reference to the node reference (e.g., &NodeRef or &OwnedNodeRef) instead of consuming/owning the full subtree: change the handler signature used in custom_enc_handlers (and any types implementing handle) to accept &NodeRef/&OwnedNodeRef, and at this dispatch site (where handler_clone.handle is invoked) pass a reference to enc_node (or Arc/Owned wrapper) rather than calling to_owned; only clone or call to_owned inside handler implementations that truly require ownership. Ensure identifiers touched include custom_enc_handlers, handler.handle, enc_node, enc_node_owned/removal, and handler_clone so callers and implementations are updated consistently.wacore/src/iq/groups.rs (1)
379-382:⚠️ Potential issue | 🟠 MajorDon't coerce unknown participant types to
Member.If the server sends an unexpected
type, this silently downgrades that participant and can erase admin/superadmin state in parsed group metadata. KeepNone => Member, but return an error for unknown strings.🐛 Suggested fix
- let participant_type = attrs - .optional_string("type") - .and_then(|s| ParticipantType::try_from(s.as_ref()).ok()) - .unwrap_or(ParticipantType::Member); + let participant_type = match attrs.optional_string("type") { + Some(s) => ParticipantType::try_from(s.as_ref())?, + None => ParticipantType::Member, + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/groups.rs` around lines 379 - 382, The current code silently maps any unknown participant type string to ParticipantType::Member; change the logic so that attrs.optional_string("type") == None still maps to ParticipantType::Member, but when optional_string returns Some(s) attempt ParticipantType::try_from(s.as_ref()) and if that returns Err propagate a parse error (do not unwrap_or Member). Replace the existing chained call that sets participant_type with code that matches on attrs.optional_string("type") and returns an Err when try_from fails (referencing participant_type, attrs.optional_string, and ParticipantType::try_from/ParticipantType::Member).src/handlers/receipt.rs (1)
28-28: 🧹 Nitpick | 🔵 TrivialMove
nodeintohandle_receiptinstead of cloning it.
nodeis not used after this call, so the extraArc::clone(&node)just adds refcount churn on a very hot path.♻️ Minimal diff
- client.handle_receipt(Arc::clone(&node)).await; + client.handle_receipt(node).await;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/receipt.rs` at line 28, The call currently clones the Arc for node (Arc::clone(&node)) when invoking client.handle_receipt(...), causing unnecessary refcount work; since node is not used afterward, move (consume) the Arc into the call instead of cloning it by passing node directly to handle_receipt (i.e., change the invocation that uses Arc::clone(&node) to pass node), and ensure the signature of handle_receipt (client.handle_receipt) accepts Arc<Node> by value so ownership transfer is correct.wacore/src/pair.rs (1)
85-98: 🧹 Nitpick | 🔵 TrivialCollapse the ACK builders into one helper.
build_ack_node_refnow mirrorsbuild_ack_nodefield-for-field, so the ACK shape has to be kept in sync in two places.♻️ Suggested direction
+ fn build_ack_node_inner(to: &str, id: &str) -> Node { + NodeBuilder::new("iq") + .attrs([ + ("to", to.to_string()), + ("id", id.to_string()), + ("type", "result".to_string()), + ]) + .build() + } + pub fn build_ack_node(request_node: &Node) -> Option<Node> { - if let (Some(to), Some(id)) = (request_node.attrs.get("from"), request_node.attrs.get("id")) - { - Some( - NodeBuilder::new("iq") - .attrs([ - ("to", to.to_string()), - ("id", id.to_string()), - ("type", "result".to_string()), - ]) - .build(), - ) - } else { - None - } + let (Some(to), Some(id)) = (request_node.attrs.get("from"), request_node.attrs.get("id")) else { + return None; + }; + Some(Self::build_ack_node_inner(to, id)) } pub fn build_ack_node_ref(request_node: &NodeRef<'_>) -> Option<Node> { let to = request_node.get_attr("from").map(|v| v.to_string_cow())?; let id = request_node.get_attr("id").and_then(|v| v.as_str())?; - Some( - NodeBuilder::new("iq") - .attrs([ - ("to", to.to_string()), - ("id", id.to_string()), - ("type", "result".to_string()), - ]) - .build(), - ) + Some(Self::build_ack_node_inner(to.as_ref(), id)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/pair.rs` around lines 85 - 98, build_ack_node_ref duplicates build_ack_node; collapse them by extracting a single helper (e.g., build_ack) that constructs the common "iq" result Node from generic inputs (like to and id) or from a reference type, then have build_ack_node_ref call that helper instead of duplicating logic; update both build_ack_node and build_ack_node_ref to use the new helper and ensure callers still receive Option<Node> or Node as before.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c33d2a43-8b56-49bb-9d81-19910e89ff5e
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (116)
Cargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rs
| // Domain type mapping — must mirror encoder's server_to_domain_type(). | ||
| // WA Web: 0=WHATSAPP, 1=LID, even+bit7=HOSTED, 129=HOSTED_LID, else throw. | ||
| // server_to_domain_type encodes Pn/unknown as the agent value directly, | ||
| // so unmapped agents round-trip as Pn with the original agent preserved. | ||
| let server = match agent { | ||
| 0 => Cow::Borrowed(crate::jid::DEFAULT_USER_SERVER), | ||
| 1 => Cow::Borrowed(crate::jid::HIDDEN_USER_SERVER), | ||
| 128 => Cow::Borrowed(crate::jid::HOSTED_SERVER), | ||
| 129 => Cow::Borrowed(crate::jid::HOSTED_LID_SERVER), | ||
| n if (n & 128) != 0 && (n & 1) == 0 => { | ||
| // WA Web treats any even number with bit 7 set as HOSTED | ||
| Cow::Borrowed(crate::jid::HOSTED_SERVER) | ||
| } | ||
| _ => { | ||
| return Err(BinaryError::AttrParse(format!( | ||
| "AD_JID invalid domain type: {agent}" | ||
| ))); | ||
| } | ||
| 0 => crate::jid::Server::Pn, | ||
| 1 => crate::jid::Server::Lid, | ||
| 128 => crate::jid::Server::Hosted, | ||
| 129 => crate::jid::Server::HostedLid, | ||
| n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, | ||
| _ => crate::jid::Server::Pn, | ||
| }; |
There was a problem hiding this comment.
Reject unsupported AD_JID domain bytes instead of coercing them to PN.
The fallback arm now turns any unknown domain-type byte into Server::Pn, which means malformed wire data gets decoded as a valid-looking WhatsApp JID instead of failing fast. That masks protocol bugs and can misroute identity handling.
Suggested fix
let server = match agent {
0 => crate::jid::Server::Pn,
1 => crate::jid::Server::Lid,
128 => crate::jid::Server::Hosted,
129 => crate::jid::Server::HostedLid,
n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted,
- _ => crate::jid::Server::Pn,
+ _ => return Err(BinaryError::InvalidNode),
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Domain type mapping — must mirror encoder's server_to_domain_type(). | |
| // WA Web: 0=WHATSAPP, 1=LID, even+bit7=HOSTED, 129=HOSTED_LID, else throw. | |
| // server_to_domain_type encodes Pn/unknown as the agent value directly, | |
| // so unmapped agents round-trip as Pn with the original agent preserved. | |
| let server = match agent { | |
| 0 => Cow::Borrowed(crate::jid::DEFAULT_USER_SERVER), | |
| 1 => Cow::Borrowed(crate::jid::HIDDEN_USER_SERVER), | |
| 128 => Cow::Borrowed(crate::jid::HOSTED_SERVER), | |
| 129 => Cow::Borrowed(crate::jid::HOSTED_LID_SERVER), | |
| n if (n & 128) != 0 && (n & 1) == 0 => { | |
| // WA Web treats any even number with bit 7 set as HOSTED | |
| Cow::Borrowed(crate::jid::HOSTED_SERVER) | |
| } | |
| _ => { | |
| return Err(BinaryError::AttrParse(format!( | |
| "AD_JID invalid domain type: {agent}" | |
| ))); | |
| } | |
| 0 => crate::jid::Server::Pn, | |
| 1 => crate::jid::Server::Lid, | |
| 128 => crate::jid::Server::Hosted, | |
| 129 => crate::jid::Server::HostedLid, | |
| n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, | |
| _ => crate::jid::Server::Pn, | |
| }; | |
| // Domain type mapping — must mirror encoder's server_to_domain_type(). | |
| // WA Web: 0=WHATSAPP, 1=LID, even+bit7=HOSTED, 129=HOSTED_LID, else throw. | |
| // server_to_domain_type encodes Pn/unknown as the agent value directly, | |
| // so unmapped agents round-trip as Pn with the original agent preserved. | |
| let server = match agent { | |
| 0 => crate::jid::Server::Pn, | |
| 1 => crate::jid::Server::Lid, | |
| 128 => crate::jid::Server::Hosted, | |
| 129 => crate::jid::Server::HostedLid, | |
| n if (n & 128) != 0 && (n & 1) == 0 => crate::jid::Server::Hosted, | |
| _ => return Err(BinaryError::InvalidNode), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/binary/src/decoder.rs` around lines 132 - 143, The match on the
incoming agent byte currently coerces any unknown domain byte to
crate::jid::Server::Pn (the server variable), which hides malformed wire data;
instead change the fallback arm of the match (the "_" case in the agent ->
server mapping in decoder.rs) to return a decoding error (or propagate an Err)
rather than constructing Server::Pn. Update the surrounding function that
performs this mapping to return a Result and emit a clear error (e.g.,
InvalidServerByte(agent) or DecodeError::InvalidDomain(agent)) when the byte is
not one of the handled values (0,1,128,129 or even+bit7==hosted), so unknown
domain bytes fail fast instead of being coerced.
| (AttrType::Jid, false, _) => { | ||
| quote! { | ||
| #field_ident: node.attrs().optional_jid(#attr_name) | ||
| #field_ident: node.attr_parser().optional_jid(#attr_name) | ||
| .ok_or_else(|| ::anyhow::anyhow!("missing required attribute '{}'", #attr_name))? | ||
| } | ||
| } | ||
| (AttrType::Jid, true, _) => { | ||
| quote! { | ||
| #field_ident: node.attrs().optional_jid(#attr_name) | ||
| #field_ident: node.attr_parser().optional_jid(#attr_name) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== optional_jid implementation ===\n'
rg -n "fn optional_jid|errors" wacore/binary/src -A8 -B4
printf '\n=== JID parsing branches in derive macro ===\n'
rg -n "AttrType::Jid" wacore/derive/src/lib.rs -A30 -B5
printf '\n=== Derived optional JID fields ===\n'
rg -n '#\[attr\(name = ".*", jid, optional\)\]' wacore/src src -C1Repository: jlucaso1/whatsapp-rust
Length of output: 19667
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the complete generated From impl pattern
printf '=== Generated From<NodeRef> impl to see full error handling ===\n'
rg -n "impl.*From.*NodeRef" wacore/derive/src/lib.rs -A100 | head -150
# Check for finish() calls in the generated code
printf '\n=== Check for finish() calls in generated output ===\n'
rg -n "\.finish\(\)" wacore/derive/src/lib.rs
# Look at actual derived structs to see if error handling exists elsewhere
printf '\n=== Sample struct using ProtocolNode derive ===\n'
rg -n "#\[derive.*ProtocolNode" wacore/src -A30 | head -80
# Check if there's error aggregation after attr parsing
printf '\n=== Look for attr_parser usage patterns in generated traits ===\n'
rg -n "attr_parser()" wacore/derive/src/lib.rs -B2 -A10 | head -100Repository: jlucaso1/whatsapp-rust
Length of output: 129
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the lib.rs file to understand the complete impl generation
wc -l wacore/derive/src/lib.rs
# Show the section where From<NodeRef> is generated (should be around the line with gen_from_impl or similar)
head -300 wacore/derive/src/lib.rs | tail -150Repository: jlucaso1/whatsapp-rust
Length of output: 7095
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where the From<NodeRef> impl is actually generated
printf '=== Searching for fn gen_from_impl or similar ===\n'
rg -n "gen_from_impl|impl.*From.*NodeRef" wacore/derive/src/lib.rs
# Read the section that generates the From impl
printf '\n=== Show complete from impl generation ===\n'
sed -n '600,854p' wacore/derive/src/lib.rs | head -200Repository: jlucaso1/whatsapp-rust
Length of output: 115
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the middle section of the file to find From impl
sed -n '350,550p' wacore/derive/src/lib.rs
printf '\n=== Now read lines 550-750 ===\n'
sed -n '550,750p' wacore/derive/src/lib.rs | head -150Repository: jlucaso1/whatsapp-rust
Length of output: 11614
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for the actual ProtocolNode impl generation
printf '=== Search for ProtocolNode trait impl generation ===\n'
grep -n "ProtocolNode for" wacore/derive/src/lib.rs
# Look at what comes after field_parsers is collected
printf '\n=== Look at what happens with field_parsers ===\n'
sed -n '265,330p' wacore/derive/src/lib.rsRepository: jlucaso1/whatsapp-rust
Length of output: 2962
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the complete try_from_node_ref impl around line 318-382
printf '=== Complete try_from_node_ref impl ===\n'
sed -n '315,380p' wacore/derive/src/lib.rsRepository: jlucaso1/whatsapp-rust
Length of output: 1953
Malformed optional JID attributes are silently dropped; required JID attributes mask parse failures as "missing".
optional_jid() records parsing errors in the parser's errors vector but returns None, and the generated try_from_node_ref impl discards the parser without calling .finish(). For optional fields, invalid wire data is silently accepted as absence. For required fields, the .ok_or_else() fallback obscures the parse error as a generic "missing required attribute" message. This weakens validation across all #[derive(ProtocolNode)] types with JID attributes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/derive/src/lib.rs` around lines 185 - 194, The generated code for
AttrType::Jid must not discard parse errors from
node.attr_parser().optional_jid; change both branches that reference
AttrType::Jid so the attr_parser result calls .finish() (or otherwise propagate
parser.finish() errors) instead of dropping the parser or wrapping with
ok_or_else that masks parse failures; for optional fields convert the
parser.finish() result into Option<Jid> (preserving parse errors), and for
required fields call parser.finish() and propagate its error directly rather
than using .ok_or_else("missing..."), ensuring parse errors stored in the
parser.errors vector are surfaced by try_from_node_ref; update the code paths
that build try_from_node_ref to handle the Result from .finish() and return the
parse error instead of hiding it.
| let stanza_id = node | ||
| .get_attr("id") | ||
| .and_then(|v| v.as_str()) | ||
| .unwrap_or_default() | ||
| .to_string(); |
There was a problem hiding this comment.
Reject device notifications that are missing id.
unwrap_or_default() turns a malformed stanza into stanza_id == "", which makes ACK/dedup logic indistinguishable from a real empty ID. Since this field is the notification’s ACK handle, parse it as required and fail fast instead.
♻️ Minimal fix
- let stanza_id = node
- .get_attr("id")
- .and_then(|v| v.as_str())
- .unwrap_or_default()
- .to_string();
+ let stanza_id = required_attr(node, "id")?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/stanza/devices.rs` around lines 314 - 318, The parsing currently
converts a missing or malformed notification id into an empty string via
unwrap_or_default, which masks missing ACK handles; update the code that builds
stanza_id (the node.get_attr("id") ... to_string() logic in stanza/devices.rs)
to treat the id as required and fail fast when absent or not a string—return an
error or early-exit the parsing function (rather than producing stanza_id ==
""), so missing id stanzas are rejected immediately and ACK/dedup logic never
receives an ambiguous empty id.
6f3fc09 to
178a9b0
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/history_sync.rs (1)
347-355: 🧹 Nitpick | 🔵 TrivialRefactor to use the new Server enum for efficient server type checks.
The current implementation uses multiple
.is_*()method calls. The PR introduces aServerenum specifically to enable efficientu8==u8comparisons. Refactoring to usejid.server()would be more performant and align with the new API design:⚡ Proposed refactor using Server enum
- // Only 1:1 conversations carry tctokens - if jid.is_group() || jid.is_newsletter() || jid.is_bot() { - return; - } - - let resolved_lid = if jid.is_lid() { - None - } else { + use wacore_binary::Server; + + // Only 1:1 conversations carry tctokens + let resolved_lid = match jid.server() { + Server::Group | Server::Newsletter | Server::Bot => return, + Server::Lid | Server::HostedLid => None, + _ => { - self.lid_pn_cache.get_current_lid(&jid.user).await + self.lid_pn_cache.get_current_lid(&jid.user).await + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/history_sync.rs` around lines 347 - 355, Replace the repeated .is_group()/ .is_newsletter()/ .is_bot() checks with a single fast server comparison using jid.server() against the Server enum (e.g. Server::Group, Server::Newsletter, Server::Bot), and similarly replace the .is_lid() check with jid.server() == Server::Lid when deciding resolved_lid; update the early return to use jid.server() matches and change the resolved_lid conditional to check jid.server() == Server::Lid so the code uses u8 comparisons via the new Server enum instead of the is_*() methods.wacore/src/iq/prekeys.rs (1)
62-74:⚠️ Potential issue | 🟠 MajorDon't coerce malformed integer payloads to
0.
extract_content_uint()currently treats missing/non-byte content as0. In this file that flows into digest parsing, so a malformed<registration>or<list><id>can be accepted as a valid response with fabricated IDs instead of failing fast.Please make this helper return a
Result(or add a strict variant) and propagate that at required call sites.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/prekeys.rs` around lines 62 - 74, extract_content_uint currently returns 0 on missing or non-bytes content which masks malformed payloads; change fn extract_content_uint(node: Option<&NodeRef<'_>>) -> Result<u32, SomeError> (or a custom error enum) so it returns Ok(u32) for the NodeContentRef::Bytes path and Err(...) for all other cases instead of unwrap_or(0); update the body to validate byte length and produce an error for malformed data, and then propagate this Result at all call sites that parse digests/registration/list ids (replace existing usages of extract_content_uint that relied on 0 with ? or explicit error handling in the digest parsing, registration and list id parsing code paths) so malformed payloads fail fast rather than being coerced to 0.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorDon't silently drop malformed
<host>entries.
filter_map(...ok()?)turns host parse failures into missing hosts. That makes a partially invalidmedia_connresponse look successful and can leave the client with an incomplete host list.🐛 Minimal fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(MediaConnHostExtended::try_from_node_ref) + .map(|result| { + result.map(|ext| MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<Vec<_>, _>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The current code silently drops malformed <host> entries because media_conn_node.get_children_by_tag("host").filter_map(|host_node| MediaConnHostExtended::try_from_node_ref(host_node).ok().map(|ext| MediaConnHost { ... })) hides parse errors; change the logic in the MediaConnHost collection so that you map each host node to a Result<MediaConnHost, Error> via MediaConnHostExtended::try_from_node_ref and then either (a) propagate the first Err upward (change the surrounding function to return a Result) or (b) collect errors and log/return a consolidated error instead of silently skipping entries—update the code that constructs hosts (the variable hosts and use of MediaConnHostExtended::try_from_node_ref) accordingly so malformed host nodes cause a visible failure or explicit handling.wacore/src/request.rs (1)
201-226: 🧹 Nitpick | 🔵 TrivialDrop the boxed
Resulthere.
parse_iq_response()is now paying a heap allocation on every call just to return a smallResult. This sits on a hot path and can stay as a plainResult<(), IqError>.♻️ Suggested fix
- pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Box<Result<(), IqError>> { + pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> { if response_node.tag == "stream:error" || response_node.tag == "xmlstreamend" { - return Box::new(Err(IqError::Disconnected(response_node.to_owned()))); + return Err(IqError::Disconnected(response_node.to_owned())); } @@ - return Box::new(Err(IqError::ServerError { code, text })); + return Err(IqError::ServerError { code, text }); } - return Box::new(Err(IqError::ServerError { + return Err(IqError::ServerError { code: 0, text: "Malformed error response".to_string(), - })); + }); } - Box::new(Ok(())) + Ok(()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/request.rs` around lines 201 - 226, The function parse_iq_response currently returns a boxed Result (Box<Result<(), IqError>>) causing unnecessary heap allocations; change the signature to return Result<(), IqError> and remove all Box::new(...) wrappers so each return yields Err(IqError::...) or Ok(()) directly (update the returns in the stream:error/xmlstreamend path, the error-type branch that builds ServerError, and the final Ok(())); ensure callers of parse_iq_response are updated to accept a plain Result<(), IqError> and the IqError variants (Disconnected, ServerError) remain unchanged.src/handlers/ib.rs (1)
123-133:⚠️ Potential issue | 🟠 MajorRoute edge-routing persistence through
DeviceCommandinstead of mutatingDevicedirectly.This new path still writes
device.edge_routing_infoviamodify_device, which bypasses the command-based state flow the rest of the repo relies on. Please persist this throughDeviceCommand+process_command()instead.As per coding guidelines "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 123 - 133, The code currently calls persistence_manager.modify_device(...) inside the async task (see routing_bytes, client.clone(), client.runtime.spawn and persistence_manager.modify_device) which mutates Device.edge_routing_info directly; instead build an appropriate DeviceCommand carrying the routing bytes (e.g. a SetEdgeRouting/UpdateEdgeRouting command) and call persistence_manager.process_command(command). Ensure the async move closure uses client_clone.persistence_manager.process_command(...) and, when reading back state, use get_device_snapshot() rather than reading Device fields directly.wacore/src/messages.rs (1)
204-224:⚠️ Potential issue | 🟡 MinorExtend
Server::Lidchecks to includeServer::HostedLidfor sender_alt mapping.Lines 204 and 220 only trigger
sender_altpopulation whenfrom.server == Server::Lid, butHostedLidis documented as a LID-based variant. If a self-message arrives with@hosted.lidserver,matches_user_or_lid()will recognize it as such (user ID match), but the alternate sender won't be mapped because the server type check excludes it. This leaves the LID-PN cache unwarmed for hosted LID recipients.Use a helper to check for both variants (e.g.,
from.is_lid() || from.server == Server::HostedLid) or create anis_lid_like()predicate that treats them equivalently.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/messages.rs` around lines 204 - 224, The Sender alternate (sender_alt) population only checks for from.server == Server::Lid, so messages from Server::HostedLid are ignored; update the condition used when building sender_alt (both in the self-message branch and the else branch) to treat HostedLid the same as Lid—e.g., replace the single-server checks with a helper predicate like from.is_lid_like() or a condition (from.is_lid() || from.server == Server::HostedLid) so that sender_alt is set via own_jid.clone()/own_lid.cloned() or attrs.optional_jid("sender_pn")/attrs.optional_jid("sender_lid") appropriately; ensure you reference and update the occurrences around the sender_alt assignments and related use of matches_user_or_lid().src/handlers/notification.rs (1)
932-942:⚠️ Potential issue | 🟠 MajorDon't treat
fromas an authoritative JID for hash-only picture updates.When the stanza only carries
hash, falling back tofrom.clone()can emit aPictureUpdatefor the wrong contact. This should stay explicitly best-effort only, or be skipped until a contact-hash lookup exists, instead of looking definitive.Based on learnings: In Rust code path
src/handlers/, for hash-based picture notifications, avoid relying on upgrading to a JID viafrom.clone()as a general fallback. This should be treated as a temporary approximation only in rare edge cases where a contact hash has no JID.src/message.rs (1)
3577-3607: 🧹 Nitpick | 🔵 TrivialDon’t reimplement the encryption-JID selection logic inside the tests.
These assertions rebuild the same PN→LID fallback branch structure that production uses, so they can stay green even if
resolve_encryption_jid()drifts in exactly the same way. Call the real helper, or extract a shared pure function and reuse it from both production and test code.Also applies to: 3695-3725, 3796-3827
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 3577 - 3607, The test is duplicating production logic for choosing the encryption JID; replace the in-test reimplementation with a call to the shared helper (e.g. resolve_encryption_jid or the existing handle_incoming_message helper) instead of reconstructing PN→LID fallback logic: call resolve_encryption_jid(&info.source, &client).await (or extract a small pure function that takes sender, sender_alt and lid_pn_cache and use that from both production and tests) so the test exercises the real code paths rather than mirroring Jid-selection logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/history_sync.rs`:
- Line 341: Remove the redundant explicit type annotation on the jid binding:
change the declaration using conv.id.parse() so the compiler infers the Jid type
instead of specifying wacore_binary::Jid; update the binding named jid (from the
match on conv.id.parse()) to rely on type inference and keep the existing
match/error handling intact.
In `@src/message.rs`:
- Around line 73-74: The code directly inspects
plaintext_node.content.as_deref() and matches on NodeContentRef; replace those
manual matches with the new NodeRef accessors by calling
plaintext_node.content_bytes() (or content_str() where appropriate) and feeding
the returned Option<&[u8]>/Option<&str> into wa::Message::decode or the
corresponding handlers; update the three other occurrences mentioned (around
lines 633-635 and 1009-1010) to use content_bytes()/content_str() instead of
matching NodeContentRef so the file no longer depends on the enum internals.
In `@src/pair.rs`:
- Around line 163-168: The match creates an unnecessary Vec via b.to_vec();
instead return and propagate a borrowed slice and pass that into do_pair_crypto:
change the extraction from
success_node.get_optional_child_by_tag(...).and_then(|n| match
n.content.as_deref() { Some(NodeContentRef::Bytes(b)) => Some(b.to_owned()? ), _
=> None, }) to return the borrowed &[u8] (i.e., Some(b)) and update the variable
name device_identity_bytes to be of type &[u8] (or an Option<&[u8]>), then call
do_pair_crypto(device_identity_bytes, ...) with the borrowed slice; ensure
lifetimes are preserved by keeping the reference live until do_pair_crypto
returns and adjust any wrapper types around success_node if needed.
In `@wacore/binary/src/node.rs`:
- Around line 589-593: Update the OwnedNodeRef doc comment to remove the
absolute claim "zero allocation beyond the buffer itself" and instead state the
precise allocation behavior: explain that OwnedNodeRef lets NodeRef borrow
directly from the decompressed buffer to avoid copying string/byte payloads, but
that NodeRef still owns container allocations (e.g., Vec/Box) for attributes and
children, so callers should expect those allocations; edit the doc comment above
the struct OwnedNodeRef (and any related examples) to reflect this nuance.
- Around line 663-679: OwnedNodeRef currently forwards content_bytes(),
content_str(), and content_nodes() to its inner NodeRef but is missing a
forwarding method for content_as_string(); add a new #[inline] pub fn
content_as_string(&self) -> Option<Cow<'_, str>> (matching the signature on
NodeRef) that simply returns self.get().content_as_string(), and include the
same doc comment style as the existing content_* methods so OwnedNodeRef has
full parity with NodeRef.
In `@wacore/src/media_retry.rs`:
- Around line 178-183: The code forces an owned String via into_owned() for
msg_id even though it is only read/compared in this zero-copy parse path; remove
the allocation by keeping the id borrowed (use the &str or Cow<'_, str> returned
from node.get_attr(...).as_str()) and propagate that borrowed type through the
decryption and validation steps. Update the binding for msg_id to be a borrowed
&str or Cow and adjust any downstream functions called here (the decryption and
validation routines referenced around this use) to accept &str/AsRef<str> (or
Cow) instead of String so you don’t allocate unnecessarily; ensure lifetimes
cover the use until after decryption/validation and remove the into_owned() call
on node.get_attr("id").
In `@wacore/src/types/events.rs`:
- Line 386: The struct field `from` is using the fully qualified type
`wacore_binary::Jid` while `Jid` is already imported and used unqualified
elsewhere; change the field type to the unqualified `Jid` (replace
`wacore_binary::Jid` with `Jid` for the `from` field) so it matches the other
usages and imports (no other changes to imports should be necessary).
---
Outside diff comments:
In `@src/handlers/ib.rs`:
- Around line 123-133: The code currently calls
persistence_manager.modify_device(...) inside the async task (see routing_bytes,
client.clone(), client.runtime.spawn and persistence_manager.modify_device)
which mutates Device.edge_routing_info directly; instead build an appropriate
DeviceCommand carrying the routing bytes (e.g. a
SetEdgeRouting/UpdateEdgeRouting command) and call
persistence_manager.process_command(command). Ensure the async move closure uses
client_clone.persistence_manager.process_command(...) and, when reading back
state, use get_device_snapshot() rather than reading Device fields directly.
In `@src/history_sync.rs`:
- Around line 347-355: Replace the repeated .is_group()/ .is_newsletter()/
.is_bot() checks with a single fast server comparison using jid.server() against
the Server enum (e.g. Server::Group, Server::Newsletter, Server::Bot), and
similarly replace the .is_lid() check with jid.server() == Server::Lid when
deciding resolved_lid; update the early return to use jid.server() matches and
change the resolved_lid conditional to check jid.server() == Server::Lid so the
code uses u8 comparisons via the new Server enum instead of the is_*() methods.
In `@src/message.rs`:
- Around line 3577-3607: The test is duplicating production logic for choosing
the encryption JID; replace the in-test reimplementation with a call to the
shared helper (e.g. resolve_encryption_jid or the existing
handle_incoming_message helper) instead of reconstructing PN→LID fallback logic:
call resolve_encryption_jid(&info.source, &client).await (or extract a small
pure function that takes sender, sender_alt and lid_pn_cache and use that from
both production and tests) so the test exercises the real code paths rather than
mirroring Jid-selection logic.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 375-385: The current code silently drops malformed <host> entries
because media_conn_node.get_children_by_tag("host").filter_map(|host_node|
MediaConnHostExtended::try_from_node_ref(host_node).ok().map(|ext| MediaConnHost
{ ... })) hides parse errors; change the logic in the MediaConnHost collection
so that you map each host node to a Result<MediaConnHost, Error> via
MediaConnHostExtended::try_from_node_ref and then either (a) propagate the first
Err upward (change the surrounding function to return a Result) or (b) collect
errors and log/return a consolidated error instead of silently skipping
entries—update the code that constructs hosts (the variable hosts and use of
MediaConnHostExtended::try_from_node_ref) accordingly so malformed host nodes
cause a visible failure or explicit handling.
In `@wacore/src/iq/prekeys.rs`:
- Around line 62-74: extract_content_uint currently returns 0 on missing or
non-bytes content which masks malformed payloads; change fn
extract_content_uint(node: Option<&NodeRef<'_>>) -> Result<u32, SomeError> (or a
custom error enum) so it returns Ok(u32) for the NodeContentRef::Bytes path and
Err(...) for all other cases instead of unwrap_or(0); update the body to
validate byte length and produce an error for malformed data, and then propagate
this Result at all call sites that parse digests/registration/list ids (replace
existing usages of extract_content_uint that relied on 0 with ? or explicit
error handling in the digest parsing, registration and list id parsing code
paths) so malformed payloads fail fast rather than being coerced to 0.
In `@wacore/src/messages.rs`:
- Around line 204-224: The Sender alternate (sender_alt) population only checks
for from.server == Server::Lid, so messages from Server::HostedLid are ignored;
update the condition used when building sender_alt (both in the self-message
branch and the else branch) to treat HostedLid the same as Lid—e.g., replace the
single-server checks with a helper predicate like from.is_lid_like() or a
condition (from.is_lid() || from.server == Server::HostedLid) so that sender_alt
is set via own_jid.clone()/own_lid.cloned() or
attrs.optional_jid("sender_pn")/attrs.optional_jid("sender_lid") appropriately;
ensure you reference and update the occurrences around the sender_alt
assignments and related use of matches_user_or_lid().
In `@wacore/src/request.rs`:
- Around line 201-226: The function parse_iq_response currently returns a boxed
Result (Box<Result<(), IqError>>) causing unnecessary heap allocations; change
the signature to return Result<(), IqError> and remove all Box::new(...)
wrappers so each return yields Err(IqError::...) or Ok(()) directly (update the
returns in the stream:error/xmlstreamend path, the error-type branch that builds
ServerError, and the final Ok(())); ensure callers of parse_iq_response are
updated to accept a plain Result<(), IqError> and the IqError variants
(Disconnected, ServerError) remain unchanged.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 2303-2307: The code slices off the first byte (bytes[1..]) from
wacore_binary::marshal::marshal_ref(node) which hardcodes the framing; instead,
pass the marshal_ref result through the same unpack helper used elsewhere to
strip/handle any framing/compression before constructing an OwnedNodeRef.
Replace the bytes[1..] usage by calling the common unpack function on the
marshaled bytes and then feed the unpacked/raw protocol bytes into
wacore_binary::OwnedNodeRef::new so ACK waiters use the canonical unpacking
logic rather than assuming a single leading byte.
- Around line 3639-3652: The current code rebuilds JID-backed attributes by
calling get_attr(...).as_str().as_ref() and wrapping into NodeValue, which
forces string materialization; instead preserve the original NodeValue to retain
zero-copy semantics—assign id, from, and participant from node.get_attr(...)
directly (cloning or taking the NodeValue as appropriate) rather than converting
to &str and reconstructing; update the bindings around id, from, and participant
(and any subsequent uses expecting NodeValue) so they use the original NodeValue
from get_attr("id"), get_attr("from"), and get_attr("participant") and avoid
as_str()/NodeValue::from round-trips.
- Around line 81-86: The match logic in NodeFilter::matches is using
attr.as_str(), which misses JID-backed attributes from NodeRef::get_attr and
breaks NodeFilter::from_jid and waiters; update the comparison inside the
attrs.iter().all closure to compare the attribute's canonical/string form (e.g.,
call attr.to_string() or otherwise obtain the attribute's full textual
representation) against v.as_str() (or v.to_string()) instead of relying solely
on attr.as_str(), so JID-backed Attr values are matched; modify the closure in
NodeFilter::matches that calls node.get_attr(...) to use this stringified
comparison.
In `@src/client/device_registry.rs`:
- Around line 301-313: The current delete_sessions_for_devices implementation
cross-products lookup.all_keys() with both Server::Lid and Server::Pn, which
deletes sessions for (user,server) pairs that never existed; instead, inspect
the resolved lookup keys (the UserLookupKeys variants returned by
resolve_lookup_keys) and only purge sessions for the actual lid and pn aliases
with their corresponding servers (e.g., call purge/delete_session for lid with
Server::Lid and pn with Server::Pn), and for the Unknown variant use the
provided caller_server (making the _server argument to clear_device_record
relevant); update delete_sessions_for_devices to iterate the UserLookupKeys
variants rather than using lookup.all_keys() combined with both servers and call
signal_cache.delete_session only for the correct (alias,server,device_id)
combinations.
In `@src/handlers/ib.rs`:
- Around line 113-116: Collapse the nested pattern into a let-chain: replace the
two nested if-let checks (child.get_optional_child("routing_info") and matching
routing_info_node.content.as_deref() to NodeContentRef::Bytes(routing_bytes))
with a single if let chain like "if let Some(routing_info_node) =
child.get_optional_child("routing_info") && let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref() { ... }", keeping the same body and
variable names (routing_info_node, routing_bytes) so behavior is unchanged.
In `@src/handlers/message.rs`:
- Around line 116-117: The current hot path clones the Arc unnecessarily with
Arc::clone(&node) when calling tx.try_send; instead move the Arc into the send
to avoid the refcount bump by calling tx.try_send(node) (i.e., pass ownership of
the Arc<Node> rather than cloning it), and ensure `node` is not referenced later
in the function (or handle the returned Err which contains the moved value if
you need to recover it).
In `@src/handlers/notification.rs`:
- Around line 555-558: The closure inside the filter_map is currently truncating
out-of-range key-index values using `v as u32`; change this to a fallible
conversion and drop invalid values by replacing the cast with a try conversion
(e.g., use `u32::try_from(v).ok()` or `v.try_into().ok()`) so the `key_index`
field of AccountSyncDevice only receives valid u32 values; update the
`optional_u64("key-index").map(...)` chain to use `and_then`/`and_then(|v|
u32::try_from(v).ok())` (or equivalent) to filter out invalid/overflowing wire
values.
In `@src/handlers/receipt.rs`:
- Line 28: client.handle_receipt(Arc::clone(&node)) creates an unnecessary Arc
refcount bump; instead move ownership of node into handle_receipt by calling
client.handle_receipt(node). Replace Arc::clone(&node) with node at the call
site referencing the variable named node and ensure no code after this line uses
node (or otherwise adjust control flow); if handle_receipt's signature requires
Arc<Node> already, no signature change is needed—just pass the owned
Arc—otherwise update the caller and any downstream call-sites to accept the
moved Arc in client.handle_receipt so ownership is transferred rather than
cloned.
In `@src/message.rs`:
- Around line 377-385: The current dispatch re-materializes the <enc> subtree
via (*enc_node).to_owned(), negating zero-copy benefits; stop creating an owned
Node at the callsite and instead hand the handler a cheap shared/borrowed
carrier: either wrap the NodeRef in an Arc (e.g., Arc<OwnedNodeRef> or
Arc<NodeRef> plus a selector/path) and pass that (replace enc_node_owned with
enc_node_arc) or change the handler API (handle) to accept a borrowed &NodeRef
so cloning happens only inside handlers that need ownership; update the spawn
block to pass the new carrier (and update handler_clone.handle signature and
implementations) and remove the to_owned() allocation.
In `@src/pair_code.rs`:
- Around line 238-245: The branch building primary_wrapped_ephemeral currently
does b.to_vec() which heap-allocates; instead, after matching
Some(NodeContentRef::Bytes(b)) with b.len() == 80, copy the 80 bytes into a
stack-backed [u8; 80] (e.g., create a [u8;80] and copy_from_slice) and return
that array (or wrap it into the expected type) so decrypt_primary_ephemeral_pub
receives a fixed-size array without a Vec allocation; update the match arm in
the reg_node.get_optional_child_by_tag(...) handling that produces
primary_wrapped_ephemeral accordingly.
In `@src/pair.rs`:
- Around line 199-206: The code currently uses unwrap_or_default() and a default
fallback which allows empty JIDs to be persisted; instead, when extracting
device JIDs from success_node (via device_node.attrs().optional_jid("jid") and
optional_jid("lid")), explicitly validate that both jid and lid are present and
well-formed and reject the pair-success stanza if either is missing or
malformed; do not return Jid::default() in the else branch—propagate an
error/early-return so SetId/SetLid cannot be called with empty identifiers.
- Around line 54-57: The code eagerly clones the byte payload with
bytes.to_vec() before validating UTF-8; instead keep the slice borrowed and
validate with std::str::from_utf8 on the bytes from grandchild.content (match
NodeContentRef::Bytes via as_deref()), then only allocate the owned String at
the final boundary when calling PairUtils::make_qr_data (e.g., call
s.to_string() or String::from(s)); update the match around grandchild.content,
the UTF-8 check (replace String::from_utf8(bytes.to_vec()) with
std::str::from_utf8(bytes)), and pass the owned String to
PairUtils::make_qr_data.
In `@src/retry.rs`:
- Line 229: handle_retry_receipt() already resolves the participant JID before
calling process_retry_key_bundle(), so remove the redundant
resolve_encryption_jid() calls inside process_retry_key_bundle() (and the other
duplicate spots at the same call sites) and reuse the resolved_jid passed in;
update process_retry_key_bundle() (and any overloads or callers at the noted
locations) to accept and use the already-resolved_jid instead of calling
resolve_encryption_jid() again, ensuring any places that still call
resolve_encryption_jid() are changed to pass the resolved_jid through.
- Around line 123-129: The code currently reads participant into an owned string
(participant_str) by using nr.get_attr("participant") or
receipt.source.sender.to_string() and later reparses it to a Jid, causing extra
allocations and potential divergence between the dedupe key and the actual Jid
used; change the logic in the branches around is_group_or_status so you parse
the participant once into a Jid (use the JID-aware accessor instead of
get_attr("participant") -> as_str -> into_owned) and reuse that parsed Jid for
building the dedupe key and any subsequent operations (update usages that
currently rely on participant_str in functions/methods that create the dedupe
key and where you later reparse to Jid), applying the same change to the other
occurrences noted (the other blocks that currently create participant_str).
In `@src/test_utils.rs`:
- Around line 8-10: Check that the marshalled Vec returned from
wacore_binary::marshal::marshal has at least one byte (guard bytes.len() >= 1)
to avoid panics, then avoid the extra copy by reusing the existing Vec buffer
instead of bytes[1..].to_vec(); e.g., take ownership of the Vec and use
Vec::split_off(1) (or otherwise remove the first byte into a separate variable)
and pass the resulting Vec into OwnedNodeRef::new so
Arc::new(OwnedNodeRef::new(...).expect(...)) receives the owned raw-protocol
bytes without an extra allocation.
In `@wacore/binary/src/decoder.rs`:
- Around line 109-115: Add a regression test that decodes a JID_PAIR containing
an unknown server and asserts that decode returns
Err(BinaryError::AttrParse(_)); specifically exercise the decoder path in
read_jid_pair by providing a user and a server token that cannot be parsed by
crate::jid::Server::try_from and assert the error matches BinaryError::AttrParse
so the new fail-fast behavior in read_jid_pair (and the JID_PAIR handling)
cannot regress.
- Around line 132-143: The match on `agent` currently assigns unsupported domain
bytes to `server` as `crate::jid::Server::Pn`, which coerces malformed wire
data; instead change the fallback arm (the `_` case in the `let server = match
agent { ... }`) to return a decoding error (e.g.,
Err(DecodeError::UnsupportedAdJid(agent)) or propagate an appropriate error
type) rather than mapping to `Server::Pn`, and update the surrounding function
signature/return path to propagate that error so malformed AD_JID bytes fail
fast.
In `@wacore/binary/src/jid.rs`:
- Around line 508-512: The actual_agent() method currently only strips the agent
for Server::Pn causing inconsistencies for agent-less server variants; update
the match on self.server in actual_agent() to return 0 for all agent-less server
variants (e.g., Server::Pn, Server::Lid, Server::Hosted, Server::HostedLid or
whatever enum variants represent `@lid`, `@hosted`, `@hosted.lid`) so callers get a
zero agent for those servers and remain consistent with the file's
parsing/formatting logic.
In `@wacore/derive/src/lib.rs`:
- Around line 185-194: The JID branches currently call
node.attrs().optional_jid(`#attr_name`) directly and thus drop parse errors;
change both branches in the AttrType::Jid cases so they call .finish() on the
parser and propagate its Result: for the required variant call
node.attrs().optional_jid(`#attr_name`).finish()?.ok_or_else(||
::anyhow::anyhow!("missing required attribute '{}'", `#attr_name`))? (so parsing
errors bubble up before checking presence) and for the optional variant call
node.attrs().optional_jid(`#attr_name`).finish()? (so malformed JIDs return an Err
instead of being treated as None); apply the same change to the other Jid branch
occurrences (e.g., the 329-336 block) in the generated try_from_node_ref
implementation.
In `@wacore/src/appstate_sync.rs`:
- Around line 98-156: The decode_patch_list_ref and decode_patch_list functions
duplicate the snapshot and external mutation hydration logic; extract that
shared logic into a new async helper (e.g., hydrate_and_process_patch_list) that
takes &self, a parsed PatchList, the validate_macs flag, and the download
closure (same FDownload type), performs the snapshot_ref download/decoding and
the per-patch external_mutations download/decoding loop, then calls
self.process_patch_list(pl, validate_macs). Replace the bodies of
decode_patch_list_ref and decode_patch_list to parse their PatchList variant,
call hydrate_and_process_patch_list(parsed_pl, validate_macs, download). Ensure
the helper returns the same Result<(Vec<Mutation>, HashState, PatchList)> and
preserves existing log messages and error handling.
In `@wacore/src/iq/blocklist.rs`:
- Around line 116-131: The parse_response implementation duplicates the
<list>/<item> traversal and warning handling that
BlocklistResponse::try_from_node_ref already encapsulates; replace the current
manual traversal in parse_response with a single call to
BlocklistResponse::try_from_node_ref (or the associated conversion method) on
the incoming response and return its Result, so parsing and warnings remain
centralized in BlocklistResponse::try_from_node_ref rather than reimplementing
the logic here; remove the filter_map/collect duplication and rely on
BlocklistResponse to produce the Vec<BlocklistEntry>.
In `@wacore/src/iq/business.rs`:
- Around line 55-59: The match arm in node_text with NodeContentRef::Bytes
currently clones bytes via b.to_vec() before UTF-8 checking; change it to
validate the borrowed bytes directly and allocate only once by using
std::str::from_utf8(b).ok().map(|s| s.to_string()) in the NodeContentRef::Bytes
arm of the node_text function so you avoid the extra Vec allocation.
In `@wacore/src/iq/chatstate.rs`:
- Around line 140-147: The parse() branch using attrs.optional_jid("from")
collapses malformed and missing JIDs into None so InvalidJid never surfaces;
update chatstate::parse (the match on attrs.optional_jid("from")) to distinguish
parse failures from missing values by either calling the required parser (e.g.,
attrs.jid("from") or a method that returns Result) and map Err to
ChatstateParseError::InvalidJid, or after optional_jid returns None inspect the
attrs parser errors/state (from optional_jid implementation) and if a JID parse
error is recorded return ChatstateParseError::InvalidJid, otherwise keep the
MissingFrom/SelfEcho logic; reference optional_jid and parse() when making the
change.
In `@wacore/src/iq/groups.rs`:
- Around line 379-382: The code currently treats any unexpected participant
"type" value as ParticipantType::Member by chaining
optional_string(...).and_then(...).unwrap_or(...); change this so that missing
attribute still defaults to ParticipantType::Member but an invalid/unknown
string returns an error instead of being coerced. Replace the chain around
attrs.optional_string("type") and ParticipantType::try_from(...) with a match or
map that: if optional_string("type") is None => ParticipantType::Member, if
Some(s) => propagate the Result from ParticipantType::try_from(s.as_ref()) (i.e.
return Err on failure) so parsing fails on unknown values; update the
surrounding function to return the proper Result type if needed.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 303-323: In try_from_node_ref (the function building
MediaConnResponseExtended), ensure attribute parse failures are propagated by
calling attrs.finish() and returning its Err before constructing/returning the
MediaConnResponseExtended; i.e., after parsing
ttl/auth_ttl/max_buckets/set_ip_token and before creating the
MediaConnResponseExtended (or after collecting hosts via
MediaConnHostExtended::try_from_node_ref), call attrs.finish().map_err(|e|
anyhow!(e))? (or otherwise propagate the error) so malformed numeric attributes
don't silently become 0/None.
In `@wacore/src/iq/node.rs`:
- Around line 22-25: The helper required_attr currently forces an allocation by
returning String; change its signature and implementation to return a
borrowed-friendly type (e.g., Cow<'_, str> or &str consistent with
optional_attr) so callers can avoid copies; update pub(crate) fn
required_attr(node: &NodeRef<'_>, key: &str) -> Result<Cow<'_, str>,
anyhow::Error> (or Result<&str, anyhow::Error> to match optional_attr) and use
node.get_attr(key).map(Cow::from).ok_or_else(|| anyhow!("missing required
attribute {key}")) so the attribute value is returned without unnecessary
allocation. Ensure any call sites of required_attr are adjusted to accept
Cow<'_, str> (or &str) as needed.
In `@wacore/src/iq/tctoken.rs`:
- Around line 281-287: The code currently allocates and reparses the jid by
calling token_node.get_attr("jid").map(|v| v.as_str().into_owned()).parse(),
which defeats the NodeRef migration performance goal; instead, use the attribute
API's typed JID conversion directly (e.g., call the NodeRef/Attribute method
that returns a Jid or result without into_owned/parse) so you read token_node's
"jid" attribute as a Jid in-place and propagate errors via map_err/ok_or_else as
before; replace the jid_str/jid parsing logic (symbols: token_node, get_attr,
jid_str, parse, Jid) with the direct-typed-attribute call to avoid allocation
and reparse.
In `@wacore/src/iq/usync.rs`:
- Around line 121-128: The parse_lid_jid helper is currently calling
attrs().optional_string("val") and then parsing that string into a Jid, which
defeats the zero-copy optimization; replace that code path by calling
attrs().optional_jid("val") to return an Option<Jid> directly. Update the
similar branches that use attrs().optional_string(...).and_then(|s|
s.parse::<Jid>().ok()) (e.g., the places handling jid, pn_jid and other val
attributes) to use attrs().optional_jid(...) instead so you avoid the
intermediate allocation and re-parse.
In `@wacore/src/pair_code.rs`:
- Around line 32-35: Replace the remaining string-based SERVER_JID usage in
pair_code.rs with the typed Server enum: import wacore_binary::Server and change
any occurrences that call SERVER_JID or SERVER_JID.to_string() in the
IQ-building code (the pair-code IQ constructors referenced around Server
targeting) to use Server::Pn instead so the builders target Server::Pn via the
new typed API; update any function parameters or Node/NodeBuilder calls that
previously accepted a String JID to accept the Server value (or convert via the
existing Server -> target API) in the same spots flagged (around the imports and
the IQ builder blocks).
In `@wacore/src/pair.rs`:
- Around line 85-98: Both build_ack_node_ref and build_ack_node construct
identical IQ "result" nodes; consolidate their logic into a single private
helper (e.g., fn make_ack_node(to: &str, id: &str) -> Node) and have
build_ack_node_ref extract "to" and "id" via request_node.get_attr(...) then
call that helper, while build_ack_node likewise forwards its to/id to the same
helper; update callers to use the unified helper through the two public
functions and remove duplicated NodeBuilder construction currently in
build_ack_node_ref and build_ack_node.
In `@wacore/src/prekeys.rs`:
- Around line 144-147: The function extract_bytes_ref currently returns Vec<u8>,
causing unnecessary heap allocations in the zero-copy parser; change its
signature to return Result<&[u8], anyhow::Error> and have it match
Some(NodeContentRef::Bytes(b)) => Ok(b) (without to_vec()). Update all callsites
in the prekey parsing code (the branches that build value and signature arrays
and any other uses noted around the value/signature handling) to accept the byte
slice, check its length, and perform a single copy into the fixed-size arrays
([u8; 32], [u8; 64]) at the final boundary instead of allocating intermediate
Vecs; ensure lifetimes from NodeRef are preserved so the returned &[u8] remains
valid for the copy.
In `@wacore/src/stanza/devices.rs`:
- Around line 317-321: The code currently uses node.get_attr("id").map(|v|
v.as_str()).unwrap_or_default().into_owned() to produce stanza_id, which
collapses missing/malformed IDs to ""—instead require and validate the id:
replace the unwrap_or_default usage for stanza_id with explicit handling of
node.get_attr("id") (and the resulting &str) and if it's missing or empty
return/propagate an error (or return None) and log context so malformed
notifications are rejected before ACK/dedup logic; reference the stanza_id
variable and the node.get_attr("id") call when making this change so the
function (the caller handling ACK/dedup) stops processing notifications with
absent/empty ids.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2b61036e-a577-474e-a47b-f3fd8994bd21
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (119)
Cargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/benches/binary_benchmark.rswacore/binary/src/attrs.rswacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rswacore/tests/binary_protocol_test.rs
|
|
||
| // Resolve to LID for storage key consistency with notification handler | ||
| let jid: wacore_binary::jid::Jid = match conv.id.parse() { | ||
| let jid: wacore_binary::Jid = match conv.id.parse() { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider removing redundant type annotation.
The explicit wacore_binary::Jid type annotation can be inferred from .parse(). Removing it would reduce verbosity:
- let jid: wacore_binary::Jid = match conv.id.parse() {
+ let jid = match conv.id.parse() {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let jid: wacore_binary::Jid = match conv.id.parse() { | |
| let jid = match conv.id.parse() { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/history_sync.rs` at line 341, Remove the redundant explicit type
annotation on the jid binding: change the declaration using conv.id.parse() so
the compiler infers the Jid type instead of specifying wacore_binary::Jid;
update the binding named jid (from the match on conv.id.parse()) to rely on type
inference and keep the existing match/error handling intact.
| let msg_id = node | ||
| .attrs() | ||
| .optional_string("id") | ||
| .get_attr("id") | ||
| .map(|v| v.as_str()) | ||
| .ok_or_else(|| anyhow!("notification missing 'id' attribute"))? | ||
| .to_string(); | ||
| .into_owned(); | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Avoid forcing an owned msg_id in this zero-copy parse path.
into_owned() allocates even though the id is only read/compared in-scope. Keep it borrowed (Cow/&str) through decryption and validation.
♻️ Proposed change
- let msg_id = node
- .get_attr("id")
- .map(|v| v.as_str())
- .ok_or_else(|| anyhow!("notification missing 'id' attribute"))?
- .into_owned();
+ let msg_id = node
+ .get_attr("id")
+ .map(|v| v.as_str())
+ .ok_or_else(|| anyhow!("notification missing 'id' attribute"))?;
@@
- let notification = decrypt_media_retry_notification(media_key, &msg_id, enc_iv, enc_p)?;
+ let notification =
+ decrypt_media_retry_notification(media_key, msg_id.as_ref(), enc_iv, enc_p)?;
@@
- if let Some(ref returned_id) = notification.stanza_id
- && returned_id != &msg_id
+ if let Some(ref returned_id) = notification.stanza_id
+ && returned_id != msg_id.as_ref()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/media_retry.rs` around lines 178 - 183, The code forces an owned
String via into_owned() for msg_id even though it is only read/compared in this
zero-copy parse path; remove the allocation by keeping the id borrowed (use the
&str or Cow<'_, str> returned from node.get_attr(...).as_str()) and propagate
that borrowed type through the decryption and validation steps. Update the
binding for msg_id to be a borrowed &str or Cow and adjust any downstream
functions called here (the decryption and validation routines referenced around
this use) to accept &str/AsRef<str> (or Cow) instead of String so you don’t
allocate unnecessarily; ensure lifetimes cover the use until after
decryption/validation and remove the into_owned() call on node.get_attr("id").
178a9b0 to
fafb553
Compare
left a comment
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fafb5538c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[default] | ||
| Pn = 0, |
There was a problem hiding this comment.
Keep default JID server empty instead of PN
Making Server::Pn the enum default changes Jid::default() from an empty sentinel to a valid-looking s.whatsapp.net JID, which alters behavior anywhere missing/invalid required JIDs fall back to unwrap_or_default() (for example AttrParserRef::jid). Those call sites can now be routed through PN-specific branches instead of staying obviously empty, so malformed stanzas may be processed as normal PN traffic rather than rejected or ignored.
Useful? React with 👍 / 👎.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/spam_report.rs (1)
88-88:⚠️ Potential issue | 🟡 MinorUse obviously fictitious phone numbers in test code.
The phone number
"5511999887766"follows a realistic Brazilian mobile number format (country code 55, São Paulo area code 11, 9-digit local number) and could potentially be real PII. Based on learnings, test code should use clearly fictitious values to avoid any risk of including real user data.🛡️ Suggested fix to use obviously fictitious values
- from_jid: Some(Jid::pn("5511999887766")), + from_jid: Some(Jid::pn("1234567890")),And at line 114:
- participant_jid: Some(Jid::pn("5511999887766")), + participant_jid: Some(Jid::pn("1234567890")),Also applies to: 114-114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/spam_report.rs` at line 88, The test uses a realistic phone number in the Jid construction (from_jid: Some(Jid::pn("5511999887766"))); replace that literal with an obviously fictitious phone number in both occurrences (the from_jid Jid::pn call at line 88 and the similar call at line 114) — e.g., use a clearly fake pattern such as repeating zeros or a reserved test prefix — so update the string passed to Jid::pn accordingly to remove any potential real PII.src/handlers/ib.rs (1)
123-133: 🛠️ Refactor suggestion | 🟠 MajorAvoid mutating
Devicedirectly from this spawned task.This still writes
edge_routing_infothroughmodify_device(...). Please route the update through aDeviceCommandso it stays on the repository’s device-state mutation path.As per coding guidelines "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 123 - 133, The spawned task must not call modify_device(...) directly; instead build a DeviceCommand that carries the routing_bytes and send it through the repository mutation path by calling client_clone.persistence_manager.process_command(command).await (construct the appropriate variant, e.g., a SetEdgeRoutingInfo or UpdateEdgeRouting command containing routing_bytes), and if you need to read state do so via client_clone.persistence_manager.get_device_snapshot(); replace the modify_device(...) invocation inside the client.runtime.spawn block with this process_command(...) flow.wacore/src/request.rs (1)
201-226: 🧹 Nitpick | 🔵 TrivialDrop the heap allocation from
parse_iq_response.
Box<Result<(), IqError>>allocates on every IQ response, but this helper only returns a small enum. ReturningResult<(), IqError>directly keeps the parse path allocation-free.♻️ Suggested change
- pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Box<Result<(), IqError>> { + pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> { if response_node.tag == "stream:error" || response_node.tag == "xmlstreamend" { - return Box::new(Err(IqError::Disconnected(response_node.to_owned()))); + return Err(IqError::Disconnected(response_node.to_owned())); } if let Some(res_type) = response_node.get_attr("type") && res_type.as_str() == "error" { let error_child = response_node.get_optional_child_by_tag(&["error"]); if let Some(error_node) = error_child { let mut parser = error_node.attrs(); let code = parser.optional_u64("code").unwrap_or(0) as u16; let text = parser .optional_string("text") .as_deref() .unwrap_or("") .to_string(); - return Box::new(Err(IqError::ServerError { code, text })); + return Err(IqError::ServerError { code, text }); } - return Box::new(Err(IqError::ServerError { + return Err(IqError::ServerError { code: 0, text: "Malformed error response".to_string(), - })); + }); } - Box::new(Ok(())) + Ok(()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/request.rs` around lines 201 - 226, The parse_iq_response function currently returns Box<Result<(), IqError>> causing unnecessary heap allocations; change its signature to return Result<(), IqError> and remove all Box::new wrappers: replace every return Box::new(Err(...)) with Err(...) and the final Box::new(Ok(())) with Ok(()). Update the function declaration parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> and ensure any callers of parse_iq_response are adjusted to accept a Result instead of a boxed Result.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorDon't silently drop malformed
<host>entries.This
filter_map(...ok()? ...)turns host-parse failures into “host absent”, so a bad<host>node yields a partial/empty host list instead of rejecting the response. That can hide server/data regressions and leave callers with an unusable media host set.Suggested fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(|host_node| { + let ext = MediaConnHostExtended::try_from_node_ref(host_node)?; + Ok(MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<_, anyhow::Error>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The code silently drops malformed <host> entries by using filter_map with MediaConnHostExtended::try_from_node_ref(...).ok() which hides parse errors; instead map each host_node into a Result<MediaConnHost, _> via MediaConnHostExtended::try_from_node_ref(host_node).map(|ext| MediaConnHost { hostname: ext.hostname, host_type: ext.host_type, fallback_hostname: ext.fallback_hostname }), then collect the iterator into a Result<Vec<MediaConnHost>, _> (e.g. .collect::<Result<Vec<_>, _>>() or using try_collect) and propagate the error from the enclosing function (adjust the return type to Result if necessary) so malformed host entries cause a failure rather than being dropped silently.src/handlers/message.rs (1)
77-118:⚠️ Potential issue | 🟠 MajorRecreate the per-chat queue when the cached sender is closed.
The worker intentionally exits on a generation change, which drops
rxbut leaves the cachedtxalive until cache expiry. After a reconnect,get_with_by_refcan hand back that closed sender and Line 117 just logs thetry_sendfailure, so messages for that chat are dropped instead of starting a fresh worker. Invalidate/rebuild the queue under the same enqueue lock and retry once onTrySendError::Closed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/message.rs` around lines 77 - 118, The cached per-chat sender (tx) can be closed after a worker exits on generation change, causing tx.try_send(node) to fail and silently drop messages; modify the enqueue path that uses get_with_by_ref so that when tx.try_send returns Err(TrySendError::Closed) you invalidate/rebuild the per-chat queue under the same enqueue lock (recreate tx/rx and respawn the worker using the existing spawn_generation logic), then retry the send once; ensure you only retry once to avoid races and preserve existing locking behavior around the queue creation.wacore/src/usync.rs (1)
163-183: 🧹 Nitpick | 🔵 TrivialAvoid reparsing JIDs from strings on this zero-copy path.
optional_string("jid")+parse::<Jid>()and the same pattern for<lid val="...">reintroduce string materialization/parsing right in the newNodeRefparser. Prefer the typed attr accessors (optional_jid("jid"),optional_jid("val")) so this stays allocation-light and keeps theServer-typed behavior consistent with the rest of the migration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/usync.rs` around lines 163 - 183, The code is reparsing JID strings via user_node.attrs().optional_string("jid") and lid_node.attrs().optional_string("val") then parse::<Jid>(), which breaks the zero-copy path; replace those with the typed accessors user_node.attrs().optional_jid("jid") and lid_node.attrs().optional_jid("val") (or the node-level optional_jid helpers) and then use the returned Jid directly when checking user_jid.server and lid_jid.server so you avoid allocation/parsing and keep Server-typed behavior consistent with the rest of the NodeRef parser.wacore/src/stanza/business.rs (1)
187-336: 🛠️ Refactor suggestion | 🟠 MajorCollapse these newly touched
NodeRefbranches into let-chains.The
remove,verified_name,profile, andsubscriptionspaths reintroduce nestedif let/else if letblocks. Please collapse them into let-chains so the parser keeps the repo’s preferred collapsible-if style throughout the migration. As per coding guidelines, "Use let-chains (if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks to maintain collapsible if patterns".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/stanza/business.rs` around lines 187 - 336, The code reintroduces nested if-let blocks for node children ("remove", "verified_name", "profile", "subscriptions"); collapse them into let-chains so the parser keeps the collapsible-if style: for each branch (remove_node, vn_node, profile_node, subs_node) use combined let-chains that bind the child and the needed attrs in a single if-let Some(...) = ... && let Some(...) = ... pattern, then return the appropriate BusinessNotificationType (e.g., BusinessNotificationType::RemoveJid / RemoveHash, VerifiedNameJid / VerifiedNameHash using VerifiedName::try_from_node, Profile / ProfileHash, Subscriptions) and construct the vectors/BusinessSubscription entries as before; reference the existing symbols remove_node, vn_node, profile_node, subs_node, BusinessNotificationType, and VerifiedName::try_from_node to locate and refactor each block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/client.rs`:
- Around line 4900-4902: Remove the local shim function node_to_owned_ref in
this module and instead import and use the shared helper from
crate::test_utils::node_to_owned_ref directly; delete the fn
node_to_owned_ref(node: Node) -> Arc<wacore_binary::OwnedNodeRef> wrapper, add a
use/import for crate::test_utils::node_to_owned_ref at the top of the file (or
replace local calls to call crate::test_utils::node_to_owned_ref) and update any
call sites in this file to reference the shared helper.
- Around line 2301-2317: The code removes the waiter from response_waiters
before attempting the fallible re-encoding (marshal_ref / OwnedNodeRef::new),
causing dropped senders on encode failure; change the logic so the waiter is
only removed after successful materialization of the payload (i.e., lock
response_waiters, get or clone the waiter without removing it, perform
marshal_ref(...) and OwnedNodeRef::new(...), and only call remove(&id) after Ok
and successful waiter.send(Arc::new(onr))); alternatively, if you prefer to keep
removal early, explicitly send an error state to the waiter in the Err branch
(using the same waiter variable) before or instead of removing it, referencing
response_waiters, marshal_ref, OwnedNodeRef::new, and waiter.send to locate the
code to modify.
In `@wacore/binary/src/jid.rs`:
- Line 627: The fast-path parser parse_jid_fast() is currently collapsing
malformed numeric parts to 0 so the new numeric overflow/format validation never
runs; modify parse_jid_fast() to detect and reject malformed numeric components
(e.g., non-digit characters or out-of-range values) by returning None instead of
silently converting them, so the code paths that call
Server::try_from(parts.server)? and the fallback validation logic will run; also
apply the same change to other fast-path uses referenced around the numeric
handling sites (the other parse_jid_fast call sites noted) so malformed inputs
like "123:abc@s.whatsapp.net" and "user.300@hosted" fall back to proper
validation and produce errors rather than turning into valid JIDs.
In `@wacore/src/appstate_sync.rs`:
- Around line 98-107: The decode_patch_list_ref and decode_multi_patch_list_ref
entrypoints are intended to be zero-copy but still trigger a deep copy because
parse_patch_list_ref and parse_patch_lists_ref call node.to_owned(); update
those parse_* functions to accept NodeRef<'_> (or otherwise parse from
references) and remove the node.to_owned() allocation so parsing operates
directly on the borrowed node tree; then adjust the callers
(decode_patch_list_ref, decode_multi_patch_list_ref) to pass the NodeRef through
to the new reference-based parse functions and ensure signature and trait bounds
(if any) are updated accordingly to preserve borrow lifetimes and avoid cloning
the stanza.
In `@wacore/src/media_retry.rs`:
- Around line 208-211: In decrypt_media_retry_notification(), validate the
enc_iv bytes before calling Nonce::from_slice to avoid a panic: after obtaining
enc_iv from the encrypt_node (the enc_iv variable) check that enc_iv.len() == 12
and return an Err(anyhow!("invalid enc_iv length: {} (expected 12)",
enc_iv.len())) (or similar error) if not; this turns the panic into a handled
error path and prevents malformed input from causing a crash.
In `@wacore/src/prekeys.rs`:
- Around line 137-139: Rename the three private helper functions to remove the
trailing "_ref" suffix: change node_to_pre_key_bundle_ref to
node_to_pre_key_bundle, node_to_pre_key_ref to node_to_pre_key, and
node_to_signed_pre_key_ref to node_to_signed_pre_key; update all internal
definitions and every call site (including the group around the other
occurrences referenced) to use the new names, preserving the existing signatures
(jid: &Jid, node: &NodeRef<'_>, etc.) and visibility, and adjust any
comments/docs that mention the old names so callers compile without API noise.
---
Outside diff comments:
In `@src/handlers/ib.rs`:
- Around line 123-133: The spawned task must not call modify_device(...)
directly; instead build a DeviceCommand that carries the routing_bytes and send
it through the repository mutation path by calling
client_clone.persistence_manager.process_command(command).await (construct the
appropriate variant, e.g., a SetEdgeRoutingInfo or UpdateEdgeRouting command
containing routing_bytes), and if you need to read state do so via
client_clone.persistence_manager.get_device_snapshot(); replace the
modify_device(...) invocation inside the client.runtime.spawn block with this
process_command(...) flow.
In `@src/handlers/message.rs`:
- Around line 77-118: The cached per-chat sender (tx) can be closed after a
worker exits on generation change, causing tx.try_send(node) to fail and
silently drop messages; modify the enqueue path that uses get_with_by_ref so
that when tx.try_send returns Err(TrySendError::Closed) you invalidate/rebuild
the per-chat queue under the same enqueue lock (recreate tx/rx and respawn the
worker using the existing spawn_generation logic), then retry the send once;
ensure you only retry once to avoid races and preserve existing locking behavior
around the queue creation.
In `@src/spam_report.rs`:
- Line 88: The test uses a realistic phone number in the Jid construction
(from_jid: Some(Jid::pn("5511999887766"))); replace that literal with an
obviously fictitious phone number in both occurrences (the from_jid Jid::pn call
at line 88 and the similar call at line 114) — e.g., use a clearly fake pattern
such as repeating zeros or a reserved test prefix — so update the string passed
to Jid::pn accordingly to remove any potential real PII.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 375-385: The code silently drops malformed <host> entries by using
filter_map with MediaConnHostExtended::try_from_node_ref(...).ok() which hides
parse errors; instead map each host_node into a Result<MediaConnHost, _> via
MediaConnHostExtended::try_from_node_ref(host_node).map(|ext| MediaConnHost {
hostname: ext.hostname, host_type: ext.host_type, fallback_hostname:
ext.fallback_hostname }), then collect the iterator into a
Result<Vec<MediaConnHost>, _> (e.g. .collect::<Result<Vec<_>, _>>() or using
try_collect) and propagate the error from the enclosing function (adjust the
return type to Result if necessary) so malformed host entries cause a failure
rather than being dropped silently.
In `@wacore/src/request.rs`:
- Around line 201-226: The parse_iq_response function currently returns
Box<Result<(), IqError>> causing unnecessary heap allocations; change its
signature to return Result<(), IqError> and remove all Box::new wrappers:
replace every return Box::new(Err(...)) with Err(...) and the final
Box::new(Ok(())) with Ok(()). Update the function declaration
parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> and
ensure any callers of parse_iq_response are adjusted to accept a Result instead
of a boxed Result.
In `@wacore/src/stanza/business.rs`:
- Around line 187-336: The code reintroduces nested if-let blocks for node
children ("remove", "verified_name", "profile", "subscriptions"); collapse them
into let-chains so the parser keeps the collapsible-if style: for each branch
(remove_node, vn_node, profile_node, subs_node) use combined let-chains that
bind the child and the needed attrs in a single if-let Some(...) = ... && let
Some(...) = ... pattern, then return the appropriate BusinessNotificationType
(e.g., BusinessNotificationType::RemoveJid / RemoveHash, VerifiedNameJid /
VerifiedNameHash using VerifiedName::try_from_node, Profile / ProfileHash,
Subscriptions) and construct the vectors/BusinessSubscription entries as before;
reference the existing symbols remove_node, vn_node, profile_node, subs_node,
BusinessNotificationType, and VerifiedName::try_from_node to locate and refactor
each block.
In `@wacore/src/usync.rs`:
- Around line 163-183: The code is reparsing JID strings via
user_node.attrs().optional_string("jid") and
lid_node.attrs().optional_string("val") then parse::<Jid>(), which breaks the
zero-copy path; replace those with the typed accessors
user_node.attrs().optional_jid("jid") and lid_node.attrs().optional_jid("val")
(or the node-level optional_jid helpers) and then use the returned Jid directly
when checking user_jid.server and lid_jid.server so you avoid allocation/parsing
and keep Server-typed behavior consistent with the rest of the NodeRef parser.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 81-86: NodeFilter::matches currently compares only the attribute's
string-backed view which breaks JID-backed attrs; update the closure in matches
(the node.get_attr(...) check) to accept either the string form or the JID form
when comparing to self.attrs values: retrieve the attr via node.get_attr(k) and
return true if attr.as_str() == v.as_str() OR if the attr exposes a JID form
(e.g. attr.as_jid()/to_jid()) whose string representation equals v.as_str();
adjust the predicate used in self.attrs.iter().all(...) accordingly so
NodeFilter::matches handles both plain-string and JID-backed attribute values.
- Around line 3638-3651: In build_ack_node, avoid converting JID-backed attrs to
String via as_str()/as_ref(); instead construct NodeValue directly from the
borrowed attribute value returned by node.get_attr so you keep the zero-copy
borrow. Replace uses like node.get_attr("id")?.as_str().as_ref() and map(|v|
NodeValue::from(v.as_str().as_ref())) for "id", "from", "participant" and the
"type" branch with direct passes of the attribute (e.g.
node.get_attr("id").map(|v| NodeValue::from(v)) or
NodeValue::from(v.as_ref()/v.as_bytes() as appropriate to NodeValue's borrow
API) so no intermediate string allocation occurs; apply the same pattern in the
typ construction while still using is_encrypt_identity_notification(node) and
respecting own_device_pn.
In `@src/client/device_registry.rs`:
- Around line 240-241: The loop that calls self.clear_device_record(user,
device.jid.server.as_str(), &record) is over-broad: when lookup.all_keys()
returns mapped keys it yields both LID and PN aliases but the code then pairs
each key with both Server::Lid and Server::Pn, deleting four addresses instead
of the real two; also Unknown users must use the caller's server rather than
defaulting. Change the logic that iterates over lookup.all_keys(): match on each
UserLookupKeys variant and only construct (user, server) pairs that are valid
for that variant (e.g., for a LID key only use Server::Lid, for a PN key only
use Server::Pn), and for UserLookupKeys::Unknown use the incoming
device.jid.server (the caller’s server) when calling clear_device_record; update
the same pattern in the block handling lines ~301-313 as well.
In `@src/handlers/ib.rs`:
- Around line 113-116: Replace the nested `if let` blocks by collapsing them
into a let-chain: combine the `if let Some(routing_info_node) =
child.get_optional_child("routing_info")` and the `if let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref()` into a single `if let ... && let ...`
conditional so the match on `routing_info_node` and its `content` (the
`NodeContentRef::Bytes(routing_bytes)` variant) are done in one expression;
update the body that currently lives inside the inner block to run under that
single combined condition.
In `@src/handlers/notification.rs`:
- Around line 555-558: The closure in the filter_map is silently truncating
large key-index values by using `v as u32`; change the logic around the call to
`optional_u64("key-index")` in the filter_map so that you reject/return None for
values > u32::MAX instead of casting, e.g. attempt a checked conversion
(try_into or compare against u32::MAX) and only set `key_index` on
AccountSyncDevice when the u64 fits in a u32; keep the rest of the filter_map
branch behavior the same so oversized key-index attributes are filtered out
rather than wrapped.
In `@src/handlers/receipt.rs`:
- Around line 22-29: In handle (async fn handle) the call
client.handle_receipt(Arc::clone(&node)).await needlessly increments the Arc;
instead pass node by value to avoid the extra refcount bump — change the call to
client.handle_receipt(node).await (remove Arc::clone usage) so the OwnedNodeRef
Arc is moved into handle_receipt directly.
In `@src/message.rs`:
- Around line 73-74: Replace the direct pattern match against NodeContentRef in
plaintext_node by using the NodeRef content accessor: call
plaintext_node.content_bytes() and, if Some, pass the bytes to
wa::Message::decode instead of matching NodeContentRef::Bytes; update all
similar spots (the other occurrences around the indicated ranges) to use
content_bytes() so the code no longer depends on NodeContentRef internals.
- Around line 377-385: The code forces an owned allocation by calling
(*enc_node).to_owned() before dispatching to custom handlers, defeating
zero-copy; change the handler API used in handler_clone.handle to accept a
borrowed NodeRef (e.g., &NodeRef<'_> or &OwnedNodeRef) so you can pass enc_node
by reference directly instead of calling to_owned(), and only clone/allocate
inside specific handler implementations that need ownership; update the handler
trait/type signatures referenced by handler_clone.handle and all implementations
to accept the borrowed reference and adjust the call site to pass enc_node (or a
reference wrapper) without to_owned().
In `@src/pair_code.rs`:
- Around line 238-245: primary_wrapped_ephemeral is currently being allocated as
a Vec<u8> after you already validated the length==80; change the code to parse
the NodeContentRef::Bytes directly into a fixed-size [u8; 80] (e.g. using
try_into) instead of to_vec(), so you can borrow a &[u8; 80] or & [u8] when
calling decrypt_primary_ephemeral_pub; update the match arm that checks
reg_node.get_optional_child_by_tag(...) and produce a [u8;80] value (or an
Option<[u8;80]>) to avoid the heap allocation and pass that by reference into
decrypt_primary_ephemeral_pub.
In `@src/pair.rs`:
- Around line 198-206: The current code silently converts missing or invalid
jid/lid into default empty Jid via optional_jid(...).unwrap_or_default(), which
allows bad device identities to be persisted; change the logic in the
success_node/device parsing (the block using device_node.attrs() and
parser.optional_jid) to explicitly validate both "jid" and "lid" and reject the
pair-success stanza if either attribute is missing or malformed instead of
returning Jid::default(); i.e., stop using unwrap_or_default() for
parser.optional_jid, propagate the parsing error (or return an early
error/response) so SetId/SetLid never receive an empty/invalid Jid.
- Around line 163-168: The code clones the `device-identity` bytes into
`device_identity_bytes` before calling `do_pair_crypto`, breaking the zero-copy
path; instead, keep a borrowed slice and pass it directly to `do_pair_crypto`.
Replace the `...NodeContentRef::Bytes(b)) => Some(b.to_vec()),` branch so it
returns `Some(b)` or `Some(b.as_slice())` (i.e., an `&[u8]`) and change the
`device_identity_bytes` binding type to a borrowed slice (or Option<&[u8]>) so
`do_pair_crypto` receives `&[u8]` for its temporary use; if `do_pair_crypto`
currently requires ownership, update its signature to accept `&[u8]` so the
zero-copy path is preserved. Ensure lifetimes line up with the decoded buffer
scope and remove the unnecessary `to_vec()` call.
- Around line 53-57: The code currently does UTF-8 validation after allocating
by calling String::from_utf8(bytes.to_vec()); instead, keep the data borrowed
for validation using std::str::from_utf8(bytes) and only allocate at the end. In
the branch that matches NodeContentRef::Bytes(bytes) inside the
child.get_children_by_tag("ref") loop, replace String::from_utf8(bytes.to_vec())
with std::str::from_utf8(bytes) and then pass r.to_owned() (or r.to_string())
into PairUtils::make_qr_data(&device_state, ...) so validation happens on the
borrowed slice and allocation occurs only for the final String.
In `@src/retry.rs`:
- Line 223: handle_retry_receipt() now computes resolved_jid and passes it into
.process_retry_key_bundle(nr, &resolved_jid, is_peer) but
process_retry_key_bundle() still performs its own lookup and derives bundle
metadata from the pre-resolve parameter, reintroducing the redundant lookup and
ambiguous contract; update process_retry_key_bundle to accept and use the
already-resolved jid for all internal operations (remove the extra resolve call
and any metadata derivation that uses the original unresolved jid parameter),
ensure all places inside process_retry_key_bundle (and any helper it calls)
compute bundle metadata from the resolved_jid, and adjust callers (e.g.,
handle_retry_receipt and other callers in the 532-567 range) to pass the
resolved_jid consistently so no duplicate lookup occurs.
In `@src/test_utils.rs`:
- Around line 8-10: The code currently slices and clones the marshalled bytes
(bytes[1..].to_vec()), causing an extra allocation and a silent panic risk; in
node_to_owned_ref, assert the leading format byte explicitly (e.g.
assert_eq!(bytes.first(), Some(&EXPECTED_FORMAT_BYTE))) then remove it in-place
(bytes.remove(0)) and pass the modified bytes Vec into OwnedNodeRef::new so no
extra to_vec allocation or fragile slicing is used; refer to the marshal call
that produces bytes, the node_to_owned_ref helper, and OwnedNodeRef::new when
making this change.
In `@wacore/appstate/src/patch_decode.rs`:
- Around line 89-99: The wrappers parse_patch_list_ref and parse_patch_lists_ref
currently call node.to_owned(), cloning the NodeRef tree; remove those clones
and forward the reference directly by making the underlying parsers accept
references (or add reference-overloads) so you can call parse_patch_list(node)
and parse_patch_lists(node) without allocation; specifically eliminate the
node.to_owned() calls in parse_patch_list_ref and parse_patch_lists_ref and
update or add signatures for parse_patch_list and parse_patch_lists to take
&NodeRef<'_> (or provide parse_patch_list_ref_impl/parse_patch_lists_ref_impl
that accept &NodeRef<'_>) so the zero-copy path is preserved.
In `@wacore/binary/src/decoder.rs`:
- Around line 132-143: The match on `agent` in decoder.rs currently coerces
unknown domain-type bytes to `crate::jid::Server::Pn`; instead, detect
unsupported AD_JID bytes in the fallback arm of the `match agent { ... }` and
return a decoding error (aligning with WA Web behavior) instead of mapping to
PN. Replace the `_ => crate::jid::Server::Pn` arm with code that yields a
`Result::Err` (e.g. a `DecodeError::UnsupportedDomain(agent)` or similar error
type used by this decoder) so malformed/unknown domain bytes fail decoding
rather than silently becoming `Server::Pn`; keep the other arms (0,1,128,129,
hosted-bit rule) unchanged.
In `@wacore/binary/src/jid.rs`:
- Around line 508-512: The actual_agent() method currently zeros the agent only
for Server::Pn, causing mismatch with Display and is_ad() which treat
Server::Lid, Server::Hosted, and Server::HostedLid as agent-less; update
actual_agent() to return 0 for Server::Pn, Server::Lid, Server::Hosted, and
Server::HostedLid (and return self.agent for other variants) so string
round-trips preserve agent-less semantics consistent with Display and is_ad().
In `@wacore/binary/src/node.rs`:
- Around line 589-593: Update the doc comment on OwnedNodeRef to remove the
absolute claim of "zero allocation beyond the buffer itself" and instead state
that while OwnedNodeRef owns the decompressed buffer and NodeRef borrows from it
(avoiding copying payload bytes/strings), NodeRef still allocates container
structures for attributes and child nodes; reference OwnedNodeRef and NodeRef so
callers understand the remaining allocation cost.
- Around line 663-679: Add a forwarding method content_as_string on the
OwnedNodeRef wrapper so callers don't need to call .get() for the lossy string
view; implement pub fn content_as_string(&self) -> Option<String> that simply
returns self.get().content_as_string(), analogous to the existing content_bytes,
content_str and content_nodes methods on OwnedNodeRef.
In `@wacore/derive/src/lib.rs`:
- Around line 185-194: The generated try_from_node_ref should create a single
attribute parser instance (e.g., let mut attrs = node.attrs();) and use that
shared parser to parse every field (calling attrs.optional_jid(`#attr_name`) /
attrs.optional_* / attrs.required_* into local variables like
parsed_<field_ident>), then call attrs.finish()? to propagate any deferred parse
errors before constructing and returning Self with those locals; update the
blocks handling AttrType::Jid (and the similar blocks around the other mentioned
range) to stop calling node.attrs() inline and instead use the shared attrs
parser and finish() as described.
In `@wacore/src/appstate_sync.rs`:
- Around line 98-105: The provided synchronous download callback type FDownload:
Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> allows callers to perform
blocking I/O on async workers; change the API or offload calls: either
(preferred) make the callback async (e.g., FDownload:
Fn(&wa::ExternalBlobReference) -> impl Future<Output=Result<Vec<u8>>> + Send +
Sync) and update call sites (decode_patch_list_ref, and the other similar
functions around lines ~219-226, ~249-257, ~543-552) to await the download; or
keep a synchronous callback but ensure every invocation of download inside
decode_patch_list_ref (and the other named functions) wraps the call in
tokio::task::spawn_blocking and awaits the JoinHandle to prevent blocking the
Tokio runtime. Ensure trait bounds include Send + 'static where needed when
using spawn_blocking.
In `@wacore/src/iq/blocklist.rs`:
- Around line 116-131: Replace the hand-rolled traversal in parse_response with
a call to BlocklistResponse::try_from_node_ref to avoid duplicating parsing
logic: in GetBlocklistSpec::parse_response, call
BlocklistResponse::try_from_node_ref(response)?, extract/convert that
BlocklistResponse into the expected Self::Response (e.g. its entries or via an
existing conversion), and return it; remove the manual list/item iteration and
warn-on-entry-failure code so all parsing is centralized in BlocklistResponse.
In `@wacore/src/iq/business.rs`:
- Around line 55-59: In node_text, avoid cloning the bytes before UTF-8
validation: for the NodeContentRef::Bytes(b) arm, validate the borrowed slice
with std::str::from_utf8(b) (or equivalent) and then allocate the String once
(e.g., map the &str to owned via to_string()/to_owned()); update the
NodeContentRef::Bytes branch in fn node_text to return the String produced from
the validated slice instead of calling b.to_vec() and String::from_utf8.
In `@wacore/src/iq/chatstate.rs`:
- Around line 140-147: The parser currently uses attrs.optional_jid("from")
which collapses malformed and missing JIDs into None and never returns
ChatstateParseError::InvalidJid; change parse() to distinguish malformed vs
missing by using the fallible lookup (e.g., attrs.jid("from") or an equivalent
method that returns Result) so you can map a parse failure to
Err(ChatstateParseError::InvalidJid), keep the existing SelfEcho check when a
"to" attr exists, and return MissingFrom only when the "from" attribute is truly
absent; update the match around attrs.optional_jid("from") to first attempt a
fallible jid parse, return InvalidJid on parse error, return the parsed jid on
success, and preserve the SelfEcho / MissingFrom branches.
In `@wacore/src/iq/groups.rs`:
- Around line 379-382: The current code silently maps any unrecognized
participant type to ParticipantType::Member; change this to propagate unknown
values instead of downgrading: update the logic around
attrs.optional_string("type") and ParticipantType::try_from to either (a)
preserve the raw string when try_from fails (e.g., store Option<String> or a
ParticipantType::Unknown(String) variant) or (b) return/propagate an error up
the caller instead of using unwrap_or(Member). Locate the transformation that
assigns participant_type (the call chain
attrs.optional_string("type").and_then(|s|
ParticipantType::try_from(s.as_ref()).ok()).unwrap_or(ParticipantType::Member))
and replace it with code that forwards the unknown value or error so
admin/superadmin state isn't silently lost.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 303-323: The attribute parser is not finalized before constructing
MediaConnResponseExtended in try_from_node_ref, so malformed numeric/string
attrs (parsed via attrs.optional_u64 / attrs.optional_string) can be silently
accepted; after reading all attributes (auth, ttl, auth_ttl, max_buckets,
ip_token, set_ip_token) call the attr parser's finalization method (e.g.,
attrs.finish() or attrs.finalize() depending on the API) and propagate any error
before proceeding to build/return the MediaConnResponseExtended; ensure the
finalization happens before using
node.get_children_by_tag(...)/MediaConnHostExtended::try_from_node_ref so
attribute parse failures are not ignored.
In `@wacore/src/iq/node.rs`:
- Around line 22-25: The helper required_attr currently forces allocation by
converting the attribute to String; change its signature and implementation to
return a borrowed Cow<'_, str> so callers can avoid allocation — update
pub(crate) fn required_attr(node: &NodeRef<'_>, key: &str) -> Result<Cow<'_,
str>, anyhow::Error> and return the attribute as a Cow::Borrowed when possible
(only allocating to Cow::Owned when necessary), preserving the same error
behavior via anyhow! when missing; adjust any call-sites that expect String to
call .into_owned() where ownership is required.
In `@wacore/src/iq/tctoken.rs`:
- Around line 281-287: The code currently extracts the "jid" attr into a String
(jid_str) then reparses it into a Jid, causing an unnecessary allocation/parse;
instead use the attribute's typed conversion (ValueRef::to_jid / the .to_jid()
method) directly from token_node.get_attr("jid") to produce a Jid without
allocating a String—replace the jid_str retrieval and subsequent parse with a
direct call that maps missing attribute or conversion errors into the same
anyhow errors, referencing token_node.get_attr("jid"), the jid/Jid binding, and
the existing error messages for consistency.
In `@wacore/src/iq/usync.rs`:
- Around line 121-128: The parse_lid_jid function (and the other similar sites)
currently calls attrs().optional_string("val") and then parses that string into
a Jid, causing unnecessary allocation; change these to use
attrs().optional_jid("val") (or the ref-based optional_jid helper your attr API
provides) so the attribute is returned directly as a Jid without formatting and
reparsing—update parse_lid_jid and the other occurrences mentioned (the blocks
around the other similar helper functions) to replace
optional_string(...).and_then(|val| val.parse::<Jid>().ok()) with a single
attrs().optional_jid("val") call.
In `@wacore/src/media_retry.rs`:
- Around line 179-183: The code currently forces an allocation by calling
.into_owned() when extracting the "id" attribute; instead keep msg_id as a
borrowed &str by removing .into_owned() and using the &str directly for
comparisons in this zero-copy parse path. Update the local binding (msg_id) to
be a &str from .get_attr("id").map(|v| v.as_str()).ok_or_else(...)? and adjust
any later uses in the function (and its signature/closures if necessary) to
accept or compare against &str rather than String so no heap allocation occurs;
keep references to the variable name msg_id to locate and change the usage.
In `@wacore/src/pair_code.rs`:
- Around line 32-34: The code still imports and uses SERVER_JID (and calls
SERVER_JID.to_string()) in the pair-code IQ builders which preserves the old
string-based path; find all occurrences of SERVER_JID and any places that set
the "to" attribute via SERVER_JID.to_string() in wacore::pair_code IQ builder
functions and replace them with the new typed Server target (use the appropriate
Server value/instance your PR introduced instead of the string JID), remove the
SERVER_JID import, and ensure the builder signatures/usage serialize the Server
target correctly when emitting the IQ "to" attribute so pair-code follows the
typed Server migration.
In `@wacore/src/pair.rs`:
- Around line 85-98: build_ack_node_ref duplicates the ACK construction logic
from build_ack_node; extract a single helper (e.g., build_ack_node_from_parts or
build_ack_node_for) that takes (&str, &str) for (to, id) and returns
Option<Node> or Node, move the NodeBuilder call (attrs ("to", "id", "type" with
value "result") and building) into that helper, then change both
build_ack_node_ref (which should extract to and id via get_attr and map/as_str)
and build_ack_node to call this new helper so there is a single source of truth
for ACK construction.
In `@wacore/src/prekeys.rs`:
- Around line 144-147: The helper extract_bytes_ref currently allocates by
converting NodeContentRef::Bytes into a Vec<u8>; change it to return a borrowed
&[u8] (Result<&[u8], anyhow::Error>) so callers can perform length checks and
copy directly into fixed-size stack arrays at the final conversion point (e.g.,
where callers construct [u8; N] or call PreKeyBundle::new()). Update all call
sites that use extract_bytes_ref to accept a slice, perform the exact-length
check, and only then copy into the fixed-size array (or clone into Vec only when
PreKeyBundle::new truly requires owned data), eliminating the intermediate heap
allocation.
In `@wacore/src/stanza/devices.rs`:
- Around line 317-321: The code currently defaults stanza_id to an empty string
when node.get_attr("id") is missing; change this to require the id attribute
instead: in the function handling device stanzas (where stanza_id is constructed
from node.get_attr("id")), detect if get_attr("id") returns None and handle it
as an error/invalid stanza (e.g., log and return/skip processing or propagate an
Err) rather than unwrap_or_default(); ensure any downstream uses of stanza_id
assume a valid non-empty id so malformed device notifications are rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ec9c7138-fa5c-4603-9996-a2e604c332af
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (120)
.gitignoreCargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/benches/binary_benchmark.rswacore/binary/src/attrs.rswacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rswacore/tests/binary_protocol_test.rs
| return Ok(Jid { | ||
| user: CompactString::from(parts.user), | ||
| server: cow_server_from_str(parts.server), | ||
| server: Server::try_from(parts.server)?, |
There was a problem hiding this comment.
The new numeric validation is still bypassed on the fast path.
The overflow check added here only runs when parse_jid_fast() returns None, but the fast parser still collapses malformed numeric parts into 0 instead of deferring to fallback validation. Inputs like 123:abc@s.whatsapp.net or user.300@hosted will still parse as different valid JIDs instead of erroring.
Also applies to: 679-683, 690-690
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/binary/src/jid.rs` at line 627, The fast-path parser parse_jid_fast()
is currently collapsing malformed numeric parts to 0 so the new numeric
overflow/format validation never runs; modify parse_jid_fast() to detect and
reject malformed numeric components (e.g., non-digit characters or out-of-range
values) by returning None instead of silently converting them, so the code paths
that call Server::try_from(parts.server)? and the fallback validation logic will
run; also apply the same change to other fast-path uses referenced around the
numeric handling sites (the other parse_jid_fast call sites noted) so malformed
inputs like "123:abc@s.whatsapp.net" and "user.300@hosted" fall back to proper
validation and produce errors rather than turning into valid JIDs.
| let enc_iv = encrypt_node | ||
| .get_optional_child_by_tag(&["enc_iv"]) | ||
| .and_then(get_bytes_content) | ||
| .and_then(get_bytes_content_ref) | ||
| .ok_or_else(|| anyhow!("missing enc_iv in encrypt node"))?; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the RustCrypto aes-gcmcrate, what happens ifNonce::from_slice(iv) receives a slice with length different from 12 bytes?
💡 Result:
If Nonce::from_slice(iv) receives a slice with length different from 12 bytes (for the standard Aes128Gcm or Aes256Gcm types, where Nonce = GenericArray<u8, U12>), it panics at runtime with an assertion failure due to length mismatch. Nonce is a type alias for GenericArray<u8, NonceSize>, and by default NonceSize = typenum::U12 (12 bytes / 96 bits), as AES-GCM recommends 96-bit nonces. The from_slice method on GenericArray<T, N> explicitly checks if the slice length equals N::to_usize (12) and panics if not, with a message like 'assertion failed: (left == right) left: <actual_length>, right: 12'. This behavior is documented in examples for similar Key::from_slice, which "panics on length mismatch", and confirmed in real-world usage errors (e.g., panics on 8 vs 12 bytes, or wrong key lengths). For non-standard nonce sizes (e.g., AesGcm<..., U16>), the required length changes accordingly, but from_slice still panics on mismatch.
Citations:
- 1: https://docs.rs/aes-gcm/latest/aes_gcm/
- 2: https://docs.rs/aes-gcm/latest/aes_gcm/type.Nonce.html
- 3: https://docs.rs/aes-gcm
- 4: AES-GCM-256 has 12B Nonce, not 8 minio/minio#14068
- 5: https://docs.rs/aes-gcm/latest/src/aes_gcm/lib.rs.html
- 6: https://docs.rs/generic-array/0.11.0/generic_array/struct.GenericArray.html
- 7: https://github.com/fizyk20/generic-array/blob/master/src/lib.rs
- 8: https://docs.rs/generic-array/0.13.2/generic_array/struct.GenericArray.html
🏁 Script executed:
# First, locate and examine the media_retry.rs file
find . -name "media_retry.rs" -type fRepository: jlucaso1/whatsapp-rust
Length of output: 93
🏁 Script executed:
# Check the code around lines 208-211 and surrounding context
head -220 wacore/src/media_retry.rs | tail -30Repository: jlucaso1/whatsapp-rust
Length of output: 1159
🏁 Script executed:
# Search for ENC_IV_SIZE definition
rg "ENC_IV_SIZE" wacore/src/media_retry.rsRepository: jlucaso1/whatsapp-rust
Length of output: 177
🏁 Script executed:
# Search for Nonce::from_slice usage in media_retry.rs
rg "Nonce::from_slice" wacore/src/media_retry.rs -A 2 -B 2Repository: jlucaso1/whatsapp-rust
Length of output: 380
🏁 Script executed:
# Find the decrypt_media_retry_notification function and see how it uses the iv parameter
rg "decrypt_media_retry_notification" wacore/src/media_retry.rs -A 15Repository: jlucaso1/whatsapp-rust
Length of output: 2170
Add guard for enc_iv length in decrypt_media_retry_notification() to prevent panic on malformed input.
The enc_iv parameter comes from untrusted XML node bytes without validation. Passing a slice with length other than 12 bytes to Nonce::from_slice(iv) will panic at runtime (the RustCrypto aes-gcm crate's GenericArray::from_slice() asserts length equality and panics on mismatch). This creates a DoS vector.
Add the length check at the function entry as shown in the suggested diff to convert the panic into a proper error.
🛡️ Suggested diff (centralized guard in decrypt function)
pub fn decrypt_media_retry_notification(
media_key: &[u8],
stanza_id: &str,
iv: &[u8],
ciphertext: &[u8],
) -> Result<wa::MediaRetryNotification> {
+ if iv.len() != ENC_IV_SIZE {
+ return Err(anyhow!(
+ "invalid enc_iv length: expected {}, got {}",
+ ENC_IV_SIZE,
+ iv.len()
+ ));
+ }
let key = derive_media_retry_key(media_key)?;
let cipher =
Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let enc_iv = encrypt_node | |
| .get_optional_child_by_tag(&["enc_iv"]) | |
| .and_then(get_bytes_content) | |
| .and_then(get_bytes_content_ref) | |
| .ok_or_else(|| anyhow!("missing enc_iv in encrypt node"))?; | |
| pub fn decrypt_media_retry_notification( | |
| media_key: &[u8], | |
| stanza_id: &str, | |
| iv: &[u8], | |
| ciphertext: &[u8], | |
| ) -> Result<wa::MediaRetryNotification> { | |
| if iv.len() != ENC_IV_SIZE { | |
| return Err(anyhow!( | |
| "invalid enc_iv length: expected {}, got {}", | |
| ENC_IV_SIZE, | |
| iv.len() | |
| )); | |
| } | |
| let key = derive_media_retry_key(media_key)?; | |
| let cipher = | |
| Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/media_retry.rs` around lines 208 - 211, In
decrypt_media_retry_notification(), validate the enc_iv bytes before calling
Nonce::from_slice to avoid a panic: after obtaining enc_iv from the encrypt_node
(the enc_iv variable) check that enc_iv.len() == 12 and return an
Err(anyhow!("invalid enc_iv length: {} (expected 12)", enc_iv.len())) (or
similar error) if not; this turns the panic into a handled error path and
prevents malformed input from causing a crash.
left a comment
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/features/groups.rs (1)
422-427: 🛠️ Refactor suggestion | 🟠 MajorUpdate
AcceptGroupInviteV4Iq::newto follow the IqSpec pattern by accepting references instead of owned values.The constructor currently takes
group_jid: Jid, code: String, admin_jid: Jid, requiring clones at the call site. Per coding guidelines, IqSpec constructors should take&JidnotJid. Update the constructor signature to accept&Jidand&str, then remove the clones at the call site.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/groups.rs` around lines 422 - 427, AcceptGroupInviteV4Iq::new currently takes owned values (group_jid: Jid, code: String, admin_jid: Jid) forcing clones at call sites; change the constructor to follow the IqSpec pattern by accepting borrowed types (e.g., new(group_jid: &Jid, code: &str, expiration: ..., admin_jid: &Jid)), update its internal implementation to clone only when it must own data, and then remove the .clone() and .to_string() calls at callers like the execute(...) invocation shown (pass &group_jid and code.as_str() or &code, and &admin_jid). Ensure you update all other call sites of AcceptGroupInviteV4Iq::new to pass references and adjust any trait bounds or lifetimes required by the IqSpec changes.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorDo not silently drop malformed
<host>entries during media_conn parsing.Line 377-Line 379 uses
.ok()?insidefilter_map, which discards host parse failures and returns partial data. This should propagate an error so corrupt server responses are explicit.🔧 Proposed fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(|host_node| { + let ext = MediaConnHostExtended::try_from_node_ref(host_node)?; + Ok(MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<Vec<_>, anyhow::Error>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The current parsing of hosts silently drops malformed <host> entries by using .ok()? inside the filter_map when calling MediaConnHostExtended::try_from_node_ref; instead convert the iterator into a Result-producing flow and propagate parse errors upward (e.g., map each host_node to MediaConnHostExtended::try_from_node_ref(...).map(|ext| MediaConnHost { ... }) and collect::<Result<Vec<MediaConnHost>, _>>() or use try_fold) so that failures in MediaConnHostExtended::try_from_node_ref are returned as Err from the media_conn parsing code rather than being filtered out; adjust the surrounding function signature to return a Result if needed.wacore/src/iq/prekeys.rs (1)
53-74:⚠️ Potential issue | 🟠 MajorDon't treat malformed digest fields as zero or empty.
extract_content_uint()andextract_content_bytes()currently collapse missing/non-byte/short payloads into0orVec::new(). That means malformed<registration>,<skey>, or<list><id>data can still parse as a seemingly valid bundle instead of failing fast, which is risky for key-bundle handling. Make the required fixed-width fields return an error when the child is missing, non-bytes, or the wrong length.Also applies to: 246-289
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/prekeys.rs` around lines 53 - 74, extract_content_uint and extract_content_bytes currently swallow missing/non-bytes/short payloads into 0 or empty Vec, letting malformed fixed-width digest fields silently succeed; change these helpers to return a Result (or Option) that fails when the node is missing, content is not NodeContentRef::Bytes, or the bytes length is incorrect for fixed-width fields (for extract_content_uint require exactly 4 bytes; for fixed-width digests require their specific length), and propagate/handle those errors in the callers (including the other occurrence around the 246-289 region) so parsing returns an error instead of producing zero/empty values for malformed registration/skey/list/id data.src/handlers/ib.rs (1)
123-133:⚠️ Potential issue | 🟠 MajorPersist edge routing via
DeviceCommand, notmodify_device.This write path still mutates
Devicedirectly inside the spawned task, which bypasses the command/snapshot flow the repo standardizes on.As per coding guidelines, "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 123 - 133, The spawned task is mutating Device via client_clone.persistence_manager.modify_device(...) which bypasses the command/snapshot flow; replace this with sending a DeviceCommand to the persistence manager (e.g., construct a DeviceCommand::SetEdgeRouting { routing_bytes: routing_bytes.clone() } or an appropriately named variant) and call client_clone.persistence_manager.process_command(command).await instead of modify_device; ensure you create/extend the DeviceCommand enum and handler to set device.edge_routing_info, and use get_device_snapshot() elsewhere when reads are needed.src/message.rs (1)
138-152:⚠️ Potential issue | 🟠 MajorSerialize retry-count updates before sending retries.
This
get→insertsequence still races when multiple detached retry tasks hit the same message at once, so two failures can observe the same count and both send a retry/PDO even after the cap should have been reached. Please guard this with a per-message lock or an atomic cache update instead of relying on best-effort ordering.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 138 - 152, The get→insert race in increment_retry_count allows concurrent tasks to increment the same cache_key past MAX_DECRYPT_RETRIES; replace the read-then-write with a serialized/atomic update: either use a per-key async lock (e.g., maintain a message_retry_locks map and acquire the lock for cache_key inside increment_retry_count before reading/updating message_retry_counts) or change message_retry_counts to store an AtomicU8 and perform an atomic fetch_update/fetch_add with a cap at MAX_DECRYPT_RETRIES; remove the separate self.message_retry_counts.get(...) / insert(...) steps and return the new count or None based on the atomic/locked result.wacore/binary/src/jid.rs (1)
82-96:⚠️ Potential issue | 🟡 MinorFast path silently accepts malformed device components.
parse_jid_fast()usesunwrap_or(0)for numeric parsing (lines 68, 88, 101, 128), which silently converts malformed inputs like"123:abc@s.whatsapp.net"to valid JIDs withdevice=0. This bypasses the validation added in the fallback path.Consider returning
Nonefromparse_jid_fast()when numeric parsing fails, allowing the fallback path to properly validate and reject malformed JIDs:🔧 Suggested fix for one instance
if let Some(pos) = colon_pos { let user_end = pos; let device_start = pos + 1; let device_slice = &s[device_start..at]; - let device = device_slice.parse::<u16>().unwrap_or(0); + let device = device_slice.parse::<u16>().ok()?; return Some(ParsedJidParts {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 82 - 96, The fast-path in parse_jid_fast currently uses unwrap_or(0) when parsing numeric components (device/agent/integrator) which silently accepts malformed values; update parse_jid_fast so that each numeric parse (the places calling parse::<u16>().unwrap_or(0)) returns None on parse failure instead of defaulting to 0, so malformed inputs like "123:abc@s.whatsapp.net" fall back to the full parser or be rejected; locate the numeric parses in parse_jid_fast and change their handling to propagate a None when parse::<u16>() fails, ensuring ParsedJidParts is only constructed when all numeric components parsed successfully.src/retry.rs (1)
43-75: 🧹 Nitpick | 🔵 TrivialConsider a macro or generic to reduce duplication between Node and NodeRef helpers.
extract_registration_id_from_nodeandextract_registration_id_from_node_refhave identical logic with only the type differing. While the test-only version may be acceptable, if both are long-term maintenance items, a small macro could unify them.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/retry.rs` around lines 43 - 75, The two functions extract_registration_id_from_node and extract_registration_id_from_node_ref duplicate logic; refactor by extracting the shared logic into a generic helper (or small macro) that abstracts over the node type and the byte-extraction function — for example create a generic function like extract_registration_id<T>(node: &T, get_bytes: fn(&T) -> Option<&[u8]>) or a macro that calls the appropriate accessor, then have extract_registration_id_from_node call that helper with Node and get_bytes_content and extract_registration_id_from_node_ref call it with NodeRef and get_bytes_content_ref, preserving the same 4-byte big-endian assembly and return type.src/client.rs (1)
4505-4508: 🛠️ Refactor suggestion | 🟠 MajorStop mutating
Devicedirectly in tests.These setups bypass
PersistenceManager::process_command()and make the tests rely on a state transition production code is explicitly not supposed to perform. Seed the PN through the command path instead, then read it back via the snapshot APIs.As per coding guidelines, "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()"Also applies to: 4587-4590
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4505 - 4508, Tests are mutating Device via pm.modify_device(...) which bypasses the intended path; instead build and send the appropriate DeviceCommand that seeds the PN (e.g., a command that sets the device's pn) to PersistenceManager::process_command(...) and then verify via pm.get_device_snapshot(...) (replace pm.modify_device usage and similar occurrences at the other spot with a DeviceCommand + pm.process_command(...) call and assertions against get_device_snapshot()).
♻️ Duplicate comments (25)
wacore/appstate/src/patch_decode.rs (1)
89-99:⚠️ Potential issue | 🟠 Major
*_refAPIs still force full clone and negate zero-copy.Line 91 and Line 98 call
to_owned(), so the new reference entry points allocate the entire node tree before parsing. That defeats the zero-copy migration goal for appstate decode paths.♻️ Proposed fix (parse directly from
&NodeRef)pub fn parse_patch_list_ref(node: &NodeRef<'_>) -> Result<PatchList> { - parse_patch_list(&node.to_owned()) + let collection = node + .get_optional_child_by_tag(&["sync", "collection"]) + .ok_or_else(|| anyhow!("missing sync/collection"))?; + parse_single_collection_ref(collection) } pub fn parse_patch_lists_ref(node: &NodeRef<'_>) -> Result<Vec<PatchList>> { - parse_patch_lists(&node.to_owned()) + let sync_node = if node.tag == "sync" { + node + } else { + node.get_optional_child("sync") + .ok_or_else(|| anyhow!("missing sync node in response"))? + }; + + let Some(children) = sync_node.children() else { + return Ok(Vec::new()); + }; + + children + .iter() + .filter(|c| c.tag == "collection") + .map(parse_single_collection_ref) + .collect() } + +fn parse_single_collection_ref(collection: &NodeRef<'_>) -> Result<PatchList> { + // Mirror parse_single_collection using NodeRef accessors to avoid ownership conversion. + // (same attribute/content parsing logic as Node variant) +}#!/bin/bash set -euo pipefail echo "Verify whether NodeRef entry points still clone:" rg -n "parse_patch_list_ref|parse_patch_lists_ref|to_owned\\(" wacore/appstate/src/patch_decode.rs -n -C 3🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/appstate/src/patch_decode.rs` around lines 89 - 99, The two zero-copy entry points parse_patch_list_ref and parse_patch_lists_ref currently call to_owned() on the NodeRef which forces a full clone and defeats zero-copy; change these functions to call the existing parse_patch_list and parse_patch_lists implementations directly using the &NodeRef (i.e., pass node by reference or adjust those parsing helpers to accept &NodeRef if needed) so no to_owned() allocation occurs—locate parse_patch_list_ref, parse_patch_lists_ref and remove the to_owned() calls, ensuring types/signatures align with parse_patch_list/parse_patch_lists to accept a NodeRef borrow.src/history_sync.rs (1)
341-341: 🧹 Nitpick | 🔵 TrivialDrop redundant type annotation on
jid.This can be inferred from
parse()usage and downstream method calls; removing it trims noise.♻️ Suggested cleanup
- let jid: wacore_binary::Jid = match conv.id.parse() { + let jid = match conv.id.parse() { Ok(j) => j, Err(_) => return, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/history_sync.rs` at line 341, The local variable declaration uses an explicit type for jid (let jid: wacore_binary::Jid = match conv.id.parse() { ... }) which is redundant; remove the type annotation and let Rust infer it by changing the binding to let jid = match conv.id.parse() { ... } (keep the existing match arms and error handling intact) so identifiers conv.id.parse() and jid are used without the unnecessary type noise.src/test_utils.rs (1)
8-13:⚠️ Potential issue | 🟡 MinorAdd explicit guard for the leading format byte before
remove(0).Line 12 assumes the buffer is non-empty and correctly prefixed; without an assertion this can panic or strip valid payload if upstream behavior changes.
Proposed fix
pub fn node_to_owned_ref(node: &Node) -> Arc<OwnedNodeRef> { - let bytes = wacore_binary::marshal::marshal(node).expect("marshal should succeed"); + let mut bytes = wacore_binary::marshal::marshal(node).expect("marshal should succeed"); // marshal() prepends a leading format byte; OwnedNodeRef::new expects raw protocol bytes - { - let mut bytes = bytes; - bytes.remove(0); - Arc::new(OwnedNodeRef::new(bytes).expect("OwnedNodeRef::new should succeed")) - } + assert!(!bytes.is_empty(), "marshal returned empty payload"); + assert_eq!(bytes[0], 0x00, "unexpected marshal format byte"); + bytes.remove(0); + Arc::new(OwnedNodeRef::new(bytes).expect("OwnedNodeRef::new should succeed")) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test_utils.rs` around lines 8 - 13, The code currently calls remove(0) on the buffer returned by wacore_binary::marshal::marshal(node) before constructing an OwnedNodeRef, which can panic or strip data if the buffer is empty or the leading format byte is different; add an explicit guard that first asserts the buffer is non-empty and then verifies the leading format byte equals the expected marshal format marker (use the marshal module's constant or documented value) before calling remove(0), and return or fail with a clear error message if the checks fail so OwnedNodeRef::new is only called with validated raw protocol bytes.wacore/src/pair.rs (1)
85-98: 🛠️ Refactor suggestion | 🟠 MajorConsolidate duplicated ACK-node construction to one helper.
build_ack_node_refduplicates the same IQ assembly logic used bybuild_ack_node, which makes future ACK shape changes easy to miss in one path.♻️ Suggested refactor
impl PairUtils { + fn build_ack_inner(to: &str, id: &str) -> Node { + NodeBuilder::new("iq") + .attrs([ + ("to", to.to_string()), + ("id", id.to_string()), + ("type", "result".to_string()), + ]) + .build() + } + pub fn build_ack_node(request_node: &Node) -> Option<Node> { if let (Some(to), Some(id)) = (request_node.attrs.get("from"), request_node.attrs.get("id")) { - Some( - NodeBuilder::new("iq") - .attrs([ - ("to", to.to_string()), - ("id", id.to_string()), - ("type", "result".to_string()), - ]) - .build(), - ) + Some(Self::build_ack_inner(to, id)) } else { None } } pub fn build_ack_node_ref(request_node: &NodeRef<'_>) -> Option<Node> { let to = request_node.get_attr("from").map(|v| v.as_str())?; let id = request_node.get_attr("id").map(|v| v.as_str())?; - Some( - NodeBuilder::new("iq") - .attrs([ - ("to", to.to_string()), - ("id", id.to_string()), - ("type", "result".to_string()), - ]) - .build(), - ) + Some(Self::build_ack_inner(to, id)) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/pair.rs` around lines 85 - 98, build_ack_node_ref duplicates the IQ assembly done in build_ack_node; update build_ack_node_ref to reuse the single canonical ACK builder to avoid divergence — either call the existing build_ack_node (passing the extracted "to" and "id" or a Node/attrs wrapper) or extract the shared construction logic into a new helper used by both build_ack_node and build_ack_node_ref; ensure the function still accepts a NodeRef<'_> and returns Option<Node>, pulling "from" and "id" the same way but delegating Node creation to the shared helper (refer to build_ack_node_ref, build_ack_node, and NodeBuilder to locate the code).wacore/src/iq/mediaconn.rs (1)
303-318:⚠️ Potential issue | 🟠 MajorAttribute parse errors are still not finalized before constructing response objects.
Line 313-Line 318 and Line 369-Line 372 read numeric attrs via
optional_u64(...)but never finalize parser errors. Malformed numeric attributes can be silently treated as0/Noneinstead of failing fast. This mirrors an earlier unresolved finding.🐛 Minimal fix shape
let set_ip_token = attrs.optional_u64("set_ip_token"); + attrs.finish()?;Apply the same pattern in
MediaConnSpec::parse_responseafter readingttl/auth_ttl/max_buckets.Verification script:
#!/bin/bash set -euo pipefail # Inspect mediaconn parsing paths sed -n '300,335p' wacore/src/iq/mediaconn.rs sed -n '355,385p' wacore/src/iq/mediaconn.rs # Inspect parser API contract rg -n "fn optional_u64|fn finish" wacore/binary/src -A6 -B6Also applies to: 359-372
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 303 - 318, The numeric attribute reads in try_from_node_ref (ttl, auth_ttl, max_buckets, set_ip_token) and in MediaConnSpec::parse_response must finalize parser errors instead of silently treating malformed numbers as 0/None; after you call attrs.optional_u64(...) for those fields, call the parser's finish() (or otherwise propagate the parser error) to surface malformed-attribute errors (mirror the existing pattern used elsewhere), e.g. read the optional_u64 fields and then invoke attrs.finish()? to return any parse error instead of continuing.wacore/derive/src/lib.rs (1)
185-194:⚠️ Potential issue | 🟠 MajorPropagate attribute parser errors in generated
try_from_node_ref.Line 185-Line 194 still generate JID parsing via
node.attrs().optional_jid(...)and Line 329-Line 335 buildSelfdirectly. If parser errors are finalized viafinish(), malformed JID attrs can be masked asNone/“missing required attribute” instead of surfacing the real parse failure. This appears to be the same unresolved issue raised earlier.🔧 Suggested direction
- Ok(Self { - #(`#field_parsers`),* - }) + let mut attrs = node.attrs(); + let parsed = Self { + #(`#field_parsers`),* + }; + attrs.finish()?; + Ok(parsed)And generate field parsers against
attrs(single parser instance) instead of repeatednode.attrs()calls.Use this to verify parser semantics and current macro output:
#!/bin/bash set -euo pipefail # Inspect attribute parser API and whether errors are finalized in finish() rg -n "fn optional_jid|fn optional_u64|fn finish" wacore/binary/src -A6 -B6 # Inspect derive code paths in question sed -n '180,215p' wacore/derive/src/lib.rs sed -n '320,340p' wacore/derive/src/lib.rsAlso applies to: 329-335
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/derive/src/lib.rs` around lines 185 - 194, The generated code currently calls node.attrs().optional_jid(...) inline and then builds Self directly, which can mask parser errors; change try_from_node_ref generation to first create a single parser instance (e.g. let attrs = node.attrs();) and then call attrs.jid(`#attr_name`)? for required JID fields and attrs.optional_jid(`#attr_name`)? for optional JID fields so parsing errors propagate via ? instead of being converted to None/missing; store each parsed value in a local binding (e.g. let `#field_ident` = ...) and use those bindings when constructing Self so all parser errors surface correctly (also apply same pattern wherever AttrType::Jid is handled).wacore/binary/src/decoder.rs (1)
111-115:⚠️ Potential issue | 🟡 MinorAdd a regression test for the new invalid-server path.
This is the new fail-fast behavior behind the typed
Servermigration, but the decoder tests still never hit it. A tiny crafted buffer assertingErr(BinaryError::AttrParse(..))here would keep the oldServer::Pnfallback from creeping back in unnoticed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/decoder.rs` around lines 111 - 115, Add a regression test that exercises the new fail-fast invalid-server path in decoder::read_value_as_string -> Server::try_from by crafting a small buffer representing a JID_PAIR whose server string is invalid; the test should call the decoder function that parses JID pairs (the code around read_value_as_string and Server::try_from in decoder.rs) and assert that it returns Err(BinaryError::AttrParse(_)) rather than falling back to Server::Pn. Ensure the test constructs the exact binary sequence that triggers the server parse branch and matches Err(BinaryError::AttrParse(..)) to prevent regressions.src/client/device_registry.rs (1)
240-241:⚠️ Potential issue | 🟠 MajorDon't cross-product aliases with both server variants.
resolve_lookup_keys()preserves which alias is LID vs PN, butdelete_sessions_for_devices()still rebuilds every lookup key under bothServer::LidandServer::Pn. That creates invalid pairs likepn@lid/lid@pnand can delete the wrong Signal session. Match onUserLookupKeysand purge only the concrete(user, server)pairs you actually resolved; forUnknown, use the caller's real server. The new server argument passed at Line 240 still has no effect becauseclear_device_record()ignores it.Also applies to: 301-307
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/device_registry.rs` around lines 240 - 241, The code is rebuilding lookup keys under both Server::Lid and Server::Pn and thus can delete the wrong sessions; update delete_sessions_for_devices()/clear_device_record() to respect the original resolution from resolve_lookup_keys(): pattern-match on UserLookupKeys (e.g., Concrete(Lid|Pn) vs Unknown) and only construct/purge the actual (user, server) pairs you resolved — for Unknown use the server argument passed by the caller instead of inventing the opposite variant — and ensure clear_device_record() actually accepts and uses that server parameter rather than ignoring it so only the intended Signal sessions are deleted.wacore/src/media_retry.rs (2)
97-107:⚠️ Potential issue | 🔴 CriticalGuard
ivlength beforeNonce::from_slice().
ivcomes from stanza bytes, andaes-gcm'sNonce::from_slice()panics on a non-12-byte slice. A malformed media-retry notification can therefore crash this path instead of returning an error.🛡️ Suggested fix
pub fn decrypt_media_retry_notification( media_key: &[u8], stanza_id: &str, iv: &[u8], ciphertext: &[u8], ) -> Result<wa::MediaRetryNotification> { + if iv.len() != ENC_IV_SIZE { + return Err(anyhow!( + "invalid enc_iv length: expected {}, got {}", + ENC_IV_SIZE, + iv.len() + )); + } let key = derive_media_retry_key(media_key)?; let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/media_retry.rs` around lines 97 - 107, The function decrypt_media_retry_notification currently calls Nonce::from_slice(iv) which will panic if iv is not 12 bytes; add a guard at the start of decrypt_media_retry_notification to validate iv.len() == 12 and return an Err (propagating an anyhow error) when it is malformed, before calling Nonce::from_slice(), so malformed stanza bytes produce a proper error instead of panicking.
178-183: 🧹 Nitpick | 🔵 TrivialKeep
msg_idborrowed in thisNodeRefparse path.
into_owned()allocates even though the stanza id is only used as AAD and compared before returning. Keeping it borrowed preserves the zero-copy benefit of this migration.♻️ Suggested change
let msg_id = node .get_attr("id") .map(|v| v.as_str()) .ok_or_else(|| anyhow!("notification missing 'id' attribute"))? - .into_owned(); @@ - let notification = decrypt_media_retry_notification(media_key, &msg_id, enc_iv, enc_p)?; + let notification = decrypt_media_retry_notification(media_key, msg_id.as_ref(), enc_iv, enc_p)?; @@ - if let Some(ref returned_id) = notification.stanza_id - && returned_id != &msg_id + if let Some(ref returned_id) = notification.stanza_id + && returned_id != msg_id.as_ref()Also applies to: 213-217
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/media_retry.rs` around lines 178 - 183, The code currently calls into_owned() on the result of node.get_attr("id") (binding msg_id) which causes an unnecessary allocation; instead keep msg_id as a borrowed &str (do not call into_owned()), propagate that borrowed &str through the NodeRef parse path where it's used for AAD and comparisons (ensure functions/structs that accept the id take &str or a lifetime-parametrized borrow), and remove the allocation in both occurrences (the one at msg_id and the duplicate later) so comparisons and AAD use the zero-copy borrowed value.wacore/src/iq/blocklist.rs (1)
116-131: 🧹 Nitpick | 🔵 TrivialReuse
BlocklistResponse::try_from_node_refinstead of re-parsing here.This reintroduces a second parser for the same wire format, so the next schema tweak can easily fix one path and leave the other stale.
♻️ Minimal cleanup
fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { - // BlocklistResponse checks for a <list> child or direct <item> children - let entries = if let Some(list) = response.get_optional_child("list") { - list.get_children_by_tag("item") - } else { - response.get_children_by_tag("item") - } - .filter_map(|item| match BlocklistEntry::try_from_node_ref(item) { - Ok(entry) => Some(entry), - Err(e) => { - warn!(target: "blocklist", "Failed to parse blocklist entry: {e}"); - None - } - }) - .collect(); - Ok(entries) + Ok(BlocklistResponse::try_from_node_ref(response)?.entries) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/blocklist.rs` around lines 116 - 131, The parse_response implementation is re-parsing the wire format; replace the manual item iteration with a call to the existing BlocklistResponse::try_from_node_ref to produce the same result and avoid duplicate parsing logic: inside parse_response, pass the incoming response NodeRef to BlocklistResponse::try_from_node_ref (or the appropriate associated try_from_node_ref for BlocklistResponse) and map/return its parsed entries (or propagate its error) instead of using BlocklistEntry::try_from_node_ref and the current filter_map/collect loop.src/pair_code.rs (1)
238-245: 🧹 Nitpick | 🔵 TrivialUse a fixed
[u8; 80]for the wrapped ephemeral.This branch already rejects every non-80-byte payload, so allocating a
Vechere only adds heap traffic before the blocking decrypt.♻️ Minimal cleanup
- let primary_wrapped_ephemeral = match reg_node + let primary_wrapped_ephemeral: [u8; 80] = match reg_node .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"]) .and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), + Some(NodeContentRef::Bytes(b)) => b.as_ref().try_into().ok(), _ => None, }) { Some(b) => b, None => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair_code.rs` around lines 238 - 245, The code allocates a Vec for the 80-byte wrapped ephemeral despite rejecting non-80 lengths; change the extraction to produce a fixed [u8; 80] instead of Vec<u8> to avoid heap allocation: in the match on reg_node.get_optional_child_by_tag(...).and_then(...), when matching Some(NodeContentRef::Bytes(b)) if b.len() == 80, copy b into a [u8; 80] (e.g., by initializing an array and copying the slice) and return that array (or wrap it in the same owning type expected by primary_wrapped_ephemeral) so downstream code that uses primary_wrapped_ephemeral works with a stack-allocated [u8; 80] rather than Vec; update any bindings or function signatures that expect Vec<u8> accordingly (refer to primary_wrapped_ephemeral, reg_node.get_optional_child_by_tag, and NodeContentRef::Bytes).wacore/src/iq/business.rs (1)
55-59: 🧹 Nitpick | 🔵 TrivialAvoid re-allocating borrowed byte content in
node_text.This still clones every byte-backed payload before UTF-8 validation, which undercuts the zero-copy migration on a hot parsing helper.
♻️ Minimal cleanup
fn node_text(node: &NodeRef<'_>) -> Option<String> { match node.content.as_deref() { Some(NodeContentRef::String(s)) => Some(s.to_string()), - Some(NodeContentRef::Bytes(b)) => String::from_utf8(b.to_vec()).ok(), + Some(NodeContentRef::Bytes(b)) => std::str::from_utf8(b).ok().map(str::to_owned), _ => None, } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/business.rs` around lines 55 - 59, The helper node_text currently clones byte content via b.to_vec() before UTF-8 validation; replace that with a zero-copy validation using std::str::from_utf8 on the borrowed bytes and only allocate after validation. Specifically, in the match arm for NodeContentRef::Bytes(b) inside fn node_text, change String::from_utf8(b.to_vec()).ok() to std::str::from_utf8(b).ok().map(|s| s.to_string()) so validation is done without cloning the byte slice first.src/handlers/ib.rs (1)
113-116: 🛠️ Refactor suggestion | 🟠 MajorCollapse
routing_infoextraction into a let-chain.This branch still reintroduces the nested
if letpattern the repo is trying to avoid.As per coding guidelines, "Use let-chains (
if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks to maintain collapsible if patterns"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/ib.rs` around lines 113 - 116, The nested extraction of routing_info_node and routing_bytes should be collapsed into a let-chain to avoid nested `if let` blocks: replace the two nested `if let` checks that call child.get_optional_child("routing_info") and match NodeContentRef::Bytes(routing_bytes) with a single `if let Some(routing_info_node) = child.get_optional_child("routing_info") && let Some(NodeContentRef::Bytes(routing_bytes)) = routing_info_node.content.as_deref()` pattern so the code in the branch uses routing_bytes directly.wacore/src/prekeys.rs (2)
137-139: 🧹 Nitpick | 🔵 TrivialDrop the
_refsuffix from the private parser helpers.Now that
&NodeRefis the only path here, the suffix just leaves migration noise in the internal API surface.Also applies to: 211-263
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/prekeys.rs` around lines 137 - 139, Rename the private parser helper node_to_pre_key_bundle_ref to node_to_pre_key_bundle (drop the _ref suffix) and do the same for the other private parser helpers in this module that carry the _ref suffix; update all internal call sites to use the new names, keep their visibility and signatures unchanged, and run cargo build/tests to ensure no references remain. Ensure you only change internal names (not public APIs) and update any doc comments or uses inside the file so compilation succeeds.
144-147: 🧹 Nitpick | 🔵 TrivialKeep prekey parsing borrowed through the fixed-size fields.
These helpers still
to_vec()borrowed bytes and then immediately length-check/copy them into arrays, which gives back much of the zero-copy win on this parser.Also applies to: 158-175, 241-257, 270-286
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/prekeys.rs` around lines 144 - 147, The helper extract_bytes_ref currently clones borrowed bytes with to_vec(), losing zero-copy benefits; change its signature to return a borrowed slice (Result<&[u8], anyhow::Error>) instead of Vec<u8>, and propagate that pattern to the other similar helpers (those at 158-175, 241-257, 270-286). Update call sites to perform a length check on the returned &[u8] and then copy into fixed-size arrays (e.g., validate len, then array.copy_from_slice(slice) or use TryFrom<&[u8]> for arrays) rather than allocating intermediate Vecs. Ensure lifetimes on NodeRef<'_> are preserved so you never call to_vec() in these parsing helpers.wacore/src/iq/usync.rs (1)
121-128: 🧹 Nitpick | 🔵 TrivialUse
optional_jid()on these JID-valued attrs.These paths still stringify and re-parse JIDs, which adds avoidable allocations right on the new zero-copy parsing path.
Also applies to: 141-145, 315-326, 555-562
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/usync.rs` around lines 121 - 128, Replace the pattern that stringifies then re-parses JIDs in parse_lid_jid: instead of calling attrs().optional_string("val").and_then(|val| val.parse::<Jid>().ok()), use the zero-copy helper attrs().optional_jid("val") so the Jid is returned directly; apply the same change to the other occurrences in this file where attrs().optional_string(...).and_then(|val| val.parse::<Jid>().ok()) is used (same parsing sites referenced in the review).wacore/src/stanza/devices.rs (1)
317-321:⚠️ Potential issue | 🟠 MajorReject device notifications that are missing
id.This still collapses a malformed stanza into
stanza_id == "", which makes ACK/dedup handling ambiguous. Treat"id"as required and fail fast here instead.Minimal fix
- let stanza_id = node - .get_attr("id") - .map(|v| v.as_str()) - .unwrap_or_default() - .into_owned(); + let stanza_id = required_attr(node, "id")?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/stanza/devices.rs` around lines 317 - 321, The code currently turns a missing "id" into an empty stanza_id; instead treat "id" as required by replacing the unwrap_or_default flow for stanza_id with a fail-fast path: call node.get_attr("id").map(|v| v.as_str().into_owned()).ok_or_else(|| /* appropriate error or early-return */) and return or reject the stanza immediately (e.g., return Err / continue / send a rejection) when the attribute is absent. Update any callers/path (the logic around stanza_id) to handle the error path accordingly so malformed device stanzas are not processed with stanza_id == "".src/message.rs (1)
376-383: 🧹 Nitpick | 🔵 TrivialCustom enc handlers still force a full
<enc>clone.
to_owned()rematerializes the entire encrypted node right on the hot path, so the zero-copy migration stops at the extension boundary. Since this PR is already carrying API breaks, it would be better to move custom handlers to&NodeRef/&OwnedNodeRefand clone only in implementations that truly need ownership.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 376 - 383, The code forces a full clone with enc_node.to_owned() before calling handler_clone.handle, breaking the zero-copy optimization; change the custom enc handler API to accept &NodeRef (or &OwnedNodeRef) so you can pass enc_node by reference without rematerializing it here (remove enc_node_owned and enc_type_owned usage), update the handler trait/method signature (e.g. handle(&self, client: ..., node: &NodeRef, info: &InfoArc) or handle(&self, client: ..., node: &OwnedNodeRef, ...)) and update all custom handler implementations to clone only when they truly need ownership; keep the runtime.spawn async move closure but capture references to enc_node/enc_type instead of calling to_owned().src/pair.rs (1)
195-203:⚠️ Potential issue | 🟠 MajorDon’t default missing
jid/lidduring pairing success.This still lets malformed
pair-successstanzas fall through withJid::default(), which then gets persisted viaSetId/SetLidand leaves device state inconsistent. Reject the stanza instead of continuing with empty identifiers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair.rs` around lines 195 - 203, The code currently uses parser.optional_jid(...).unwrap_or_default() when extracting jid and lid from success_node, which lets malformed pair-success stanzas fall through with Jid::default() and be persisted; change this to validate presence and reject the stanza early: call parser.optional_jid("jid") and parser.optional_jid("lid") and if either is None return an error/abort processing of the pair-success (do not continue to the SetId/SetLid persistence path), or propagate a parsing error so the stanza is rejected; locate this logic around success_node.get_optional_child_by_tag(&["device"]) and replace the unwrap_or_default usage with explicit presence checks that abort on missing identifiers.src/retry.rs (1)
566-567:⚠️ Potential issue | 🟡 MinorThe duplicate
resolve_encryption_jid()call remains.Line 223 passes
&resolved_jid(already resolved at line 194) toprocess_retry_key_bundle, but line 566 callsresolve_encryption_jid(requester_jid)again. The parameter is namedrequester_jidbut receives the already-resolved JID, making this redundant.🔧 Suggested fix
async fn process_retry_key_bundle( &self, node: &NodeRef<'_>, - requester_jid: &wacore_binary::Jid, + resolved_jid: &wacore_binary::Jid, is_peer: bool, ) -> Result<(), anyhow::Error> { // ... keys extraction ... - let resolved_jid = self.resolve_encryption_jid(requester_jid).await; let signal_address = resolved_jid.to_protocol_address();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/retry.rs` around lines 566 - 567, The code calls resolve_encryption_jid() twice: once to produce resolved_jid and again later with requester_jid (which is already resolved), leading to redundant work and possible confusion; update the call site that currently does resolve_encryption_jid(requester_jid) so it uses the existing resolved_jid (or &resolved_jid) and its to_protocol_address() value (signal_address) when calling process_retry_key_bundle and elsewhere, removing the duplicate resolve_encryption_jid invocation and ensuring parameter names match the already-resolved value.wacore/binary/src/node.rs (2)
589-593: 🧹 Nitpick | 🔵 TrivialThe allocation claim in the doc comment is still overstated.
NodeRefallocatesVecfor attrs andBox<NodeVec>for children. The "zero allocation beyond the buffer itself" claim misleads callers about the actual cost model.📝 Suggested doc update
-/// A decoded node that owns its decompressed buffer. The inner `NodeRef` -/// borrows directly from the buffer — zero allocation beyond the buffer itself. +/// A decoded node that owns its decompressed buffer. The inner `NodeRef` +/// borrows string and byte payloads directly from the buffer, avoiding copies +/// of those values. Note that `NodeRef` still allocates containers (`Vec`/`Box`) +/// for attributes and children.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/node.rs` around lines 589 - 593, The doc comment on OwnedNodeRef overstates allocations — update the comment on struct OwnedNodeRef to remove the phrase "zero allocation beyond the buffer itself" and explicitly state that the inner NodeRef still allocates a Vec for attrs and a Box<NodeVec> for children; reference the types NodeRef, attrs and NodeVec in the comment and briefly describe the cheap sharing via Arc<OwnedNodeRef> while clarifying the actual allocation costs.
663-679: 🧹 Nitpick | 🔵 Trivial
content_as_string()is still not forwarded onOwnedNodeRef.
NodeRefhascontent_as_string()(line 525), butOwnedNodeRefonly forwardscontent_bytes(),content_str(), andcontent_nodes(). This gap forces callers to use.get()for string content extraction.♻️ Add missing forwarding method
impl OwnedNodeRef { + /// Extract text content, handling both String and Bytes (lossy UTF-8). + #[inline] + pub fn content_as_string(&self) -> Option<CompactString> { + self.get().content_as_string() + } + /// Zero-copy byte content, if this node has Bytes content. #[inline] pub fn content_bytes(&self) -> Option<&[u8]> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/node.rs` around lines 663 - 679, OwnedNodeRef is missing a forwarding method for NodeRef::content_as_string(), forcing callers to call .get() manually; add a pub fn content_as_string(&self) -> Option<String> (matching NodeRef's signature) to OwnedNodeRef that simply returns self.get().content_as_string() so OwnedNodeRef mirrors NodeRef's API (refer to OwnedNodeRef, NodeRef, content_as_string, and get()).wacore/binary/src/jid.rs (1)
508-513:⚠️ Potential issue | 🟠 Major
actual_agent()should return0for all agent-less servers (Pn, Lid, Hosted, HostedLid).The method currently only matches
Server::Pnbut the codebase treats Lid, Hosted, and HostedLid as agent-less: the Display impl skips agent output for these servers (lines 714–719), and the encoder uses fixed domain_type values (1, 128, 129) instead of the agent field. Returning the agent field for these servers creates an inconsistency and potential for leaking hidden state if the method is used in future code.Additionally,
normalize_for_prekey_bundle()(lines 538–540) has the same gap—it only normalizesPn | Lidbut notHosted | HostedLid.Suggested fix
pub fn actual_agent(&self) -> u8 { match self.server { - Server::Pn => 0, + Server::Pn | Server::Lid | Server::Hosted | Server::HostedLid => 0, _ => self.agent, } }Consider also updating
normalize_for_prekey_bundle()to includeHosted | HostedLid.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 508 - 513, The actual_agent() function currently only zeroes the agent for Server::Pn; update its match to treat all agent-less server variants (Server::Pn, Server::Lid, Server::Hosted, Server::HostedLid) as returning 0 instead of self.agent, and likewise update normalize_for_prekey_bundle() to include Server::Hosted and Server::HostedLid in the branch that normalizes to agent 0 so behavior matches the Display and encoder logic and prevents leaking hidden state.src/client.rs (1)
4900-4902: 🧹 Nitpick | 🔵 TrivialDrop the redundant
node_to_owned_refwrapper.This shim only forwards to
crate::test_utils::node_to_owned_ref, so it is just another indirection to keep in sync.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4900 - 4902, Remove the redundant shim function node_to_owned_ref that simply forwards to crate::test_utils::node_to_owned_ref: delete the fn node_to_owned_ref(node: Node) -> Arc<wacore_binary::OwnedNodeRef> { crate::test_utils::node_to_owned_ref(&node) } definition and update any call sites to call crate::test_utils::node_to_owned_ref(&node) directly (adjust arguments if needed), ensuring imports/uses still compile after removal.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9a7b265a-7131-4c02-b250-9865f3d2b4dc
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (120)
.gitignoreCargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/benches/binary_benchmark.rswacore/binary/src/attrs.rswacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rswacore/tests/binary_protocol_test.rs
| client | ||
| .wait_for_event(15, |e| { | ||
| matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) | ||
| matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider centralizing the w:gp2 notification predicate.
The same Event::Notification type check is duplicated several times; a shared helper would reduce drift risk in future node-API migrations.
Also applies to: 429-429, 551-551
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/tests/memory_soak.rs` at line 409, The matched predicate checking
for the `w:gp2` notification is duplicated; create a single helper such as a
free function is_gp2_notification(event: &Event) -> bool (or an Event method
like Event::is_gp2_notification) that encapsulates matches!(e,
Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() ==
"w:gp2")), then replace all duplicate occurrences (e.g., the match at
tests/e2e/tests/memory_soak.rs using matches!(e, Event::Notification(...))) with
calls to that helper to centralize the logic.
| .wait_for_event(10, |e| { | ||
| matches!(e, Event::Message(msg, _) if msg.conversation.is_some()) | ||
| || matches!(e, Event::Notification(node) if node.attrs.get("type").is_some_and(|v| v == "w:gp2")) | ||
| || matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Optional: reuse a shared group-notification matcher here too.
These inline w:gp2 checks duplicate logic already captured in test helpers; reusing one matcher would keep behavior consistent across e2e suites.
Also applies to: 436-436
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/e2e/tests/offline_groups.rs` at line 147, The inline matcher checking
for Event::Notification(...).get_attr("type") == "w:gp2" duplicates existing
shared group-notification matcher; replace the inline closure (matches!(e,
Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() ==
"w:gp2"))) with the shared helper matcher (e.g., group_notification() or the
project's is_group_notification matcher) so tests reuse the common logic; update
both occurrences (around the current match and at the other occurrence) to call
that helper instead of repeating the attribute check.
| pub async fn decode_patch_list_ref<FDownload>( | ||
| &self, | ||
| stanza_root: &NodeRef<'_>, | ||
| download: FDownload, | ||
| validate_macs: bool, | ||
| ) -> Result<(Vec<Mutation>, HashState, PatchList)> | ||
| where | ||
| FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, | ||
| { | ||
| let mut pl = parse_patch_list_ref(stanza_root)?; | ||
|
|
||
| // Download external snapshot if present (matches WhatsApp Web behavior) | ||
| if pl.snapshot.is_none() | ||
| && let Some(ext) = &pl.snapshot_ref | ||
| && let Ok(data) = download(ext) | ||
| && let Ok(snapshot) = wa::SyncdSnapshot::decode(data.as_slice()) | ||
| { | ||
| pl.snapshot = Some(snapshot); | ||
| } | ||
|
|
||
| // Download external mutations for each patch (matches WhatsApp Web behavior) | ||
| for patch in &mut pl.patches { | ||
| if let Some(ext) = &patch.external_mutations { | ||
| let patch_version = patch.version.as_ref().and_then(|v| v.version).unwrap_or(0); | ||
| match download(ext) { | ||
| Ok(data) => match wa::SyncdMutations::decode(data.as_slice()) { | ||
| Ok(ext_mutations) => { | ||
| log::trace!( | ||
| target: "AppState", | ||
| "Downloaded external mutations for patch v{}: {} mutations (inline had {})", | ||
| patch_version, | ||
| ext_mutations.mutations.len(), | ||
| patch.mutations.len() | ||
| ); | ||
| patch.mutations = ext_mutations.mutations; | ||
| } | ||
| Err(e) => { | ||
| log::warn!( | ||
| target: "AppState", | ||
| "Failed to decode external mutations for patch v{}: {}", | ||
| patch_version, | ||
| e | ||
| ); | ||
| } | ||
| }, | ||
| Err(e) => { | ||
| log::warn!( | ||
| target: "AppState", | ||
| "Failed to download external mutations for patch v{}: {}", | ||
| patch_version, | ||
| e | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| self.process_patch_list(pl, validate_macs).await | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Code duplication: decode_patch_list_ref and decode_patch_list have identical download/hydration logic.
Lines 108-153 duplicate lines 169-214 (snapshot download, external mutations loop). The multi-collection variants correctly use the shared process_patch_lists helper, but the single-collection variants don't benefit from this refactor.
Consider extracting a hydrate_patch_list(&mut pl, &download) helper that both decode_patch_list_ref and decode_patch_list call after parsing, to keep behavior aligned as this code evolves.
♻️ Sketch of shared hydration helper
fn hydrate_patch_list<FDownload>(
pl: &mut PatchList,
download: &FDownload,
) where
FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync,
{
// Download external snapshot if present
if pl.snapshot.is_none()
&& let Some(ext) = &pl.snapshot_ref
&& let Ok(data) = download(ext)
&& let Ok(snapshot) = wa::SyncdSnapshot::decode(data.as_slice())
{
pl.snapshot = Some(snapshot);
}
// Download external mutations for each patch
for patch in &mut pl.patches {
if let Some(ext) = &patch.external_mutations {
// ... existing logic ...
}
}
}Then both entrypoints become:
let mut pl = parse_patch_list_ref(stanza_root)?;
hydrate_patch_list(&mut pl, &download);
self.process_patch_list(pl, validate_macs).await🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/appstate_sync.rs` around lines 98 - 156, The snapshot/mutations
download logic duplicated between decode_patch_list_ref and decode_patch_list
should be moved into a shared helper (e.g., fn hydrate_patch_list<FDownload>(pl:
&mut PatchList, download: &FDownload) where FDownload:
Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync) that performs
the snapshot_ref decode and per-patch external_mutations download/decoding and
logging; call parse_patch_list_ref (and the single-collection parse helper in
decode_patch_list) to get a mutable PatchList, call hydrate_patch_list(&mut pl,
&download), then pass pl into self.process_patch_list(pl, validate_macs).await
so both entrypoints reuse identical hydration behavior.
| pub async fn decode_patch_list_ref<FDownload>( | ||
| &self, | ||
| stanza_root: &NodeRef<'_>, | ||
| download: FDownload, | ||
| validate_macs: bool, | ||
| ) -> Result<(Vec<Mutation>, HashState, PatchList)> | ||
| where | ||
| FDownload: Fn(&wa::ExternalBlobReference) -> Result<Vec<u8>> + Send + Sync, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
FDownload callback design permits blocking I/O on async threads — consider documenting expectation.
The FDownload: Fn(...) -> Result<Vec<u8>> signature is synchronous, but it's invoked directly from async methods without spawn_blocking. Current callers use pre-downloaded caches (safe), but the trait allows passing blocking implementations.
Since the past review flagged this and the current callers are safe, consider adding a doc comment clarifying the expectation:
/// # Note
/// The `download` callback must be non-blocking (e.g., a cache lookup).
/// Blocking I/O implementations will block the async runtime.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/appstate_sync.rs` around lines 98 - 105, The FDownload callback
for decode_patch_list_ref is synchronous (FDownload: Fn(...) -> Result<Vec<u8>>)
but used from async code, which permits callers to supply blocking I/O; add a
doc comment on the decode_patch_list_ref function clarifying that the download
callback MUST be non-blocking (e.g., cache lookup) and that blocking I/O will
block the async runtime, so callers should either provide a non-blocking
implementation or change to an async/spawn_blocking approach if they need
blocking I/O.
|
|
||
| fn build_iq(&self) -> InfoQuery<'static> { | ||
| let mut iq = InfoQuery::get("w:p", Jid::new("", SERVER_JID), None); | ||
| let mut iq = InfoQuery::get("w:p", Jid::new("", Server::Pn), None); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add an explicit assertion for keepalive to JID wire contract.
Given the switch to Jid::new("", Server::Pn) at Line 42, add a test assertion that the IQ target remains s.whatsapp.net to lock behavior.
Proposed test hardening
#[test]
fn test_keepalive_spec_build_iq() {
let spec = KeepaliveSpec::new();
let iq = spec.build_iq();
assert_eq!(iq.namespace, "w:p");
assert_eq!(iq.query_type, crate::request::InfoQueryType::Get);
+ assert_eq!(iq.to.to_string(), "s.whatsapp.net");
assert!(iq.content.is_none());
assert!(iq.timeout.is_none());
}Also applies to: 61-69
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/iq/keepalive.rs` at line 42, The change uses Jid::new("",
Server::Pn) when building keepalive IQs (InfoQuery::get), but we must harden the
test to assert the resulting IQ "to" JID remains the WhatsApp server host
("s.whatsapp.net"). Add an explicit assertion in the keepalive tests (around the
code constructing InfoQuery::get and the related block at the other occurrence)
that inspects the produced IQ's to/to_string() or its Jid and compares it to the
expected s.whatsapp.net value, so InfoQuery::get with Jid::new("", Server::Pn)
always yields the correct target host.
| #[serde(skip)] | ||
| Notification(Arc<OwnedNodeRef>), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Serde's #[serde(skip)] on an enum variant cause serializing that variant to return an error rather than serializing the variant with omitted fields?
💡 Result:
No, Serde's #[serde(skip)] on an enum variant causes attempting to serialize that variant to return an error, rather than serializing it with omitted fields. The official documentation states: "Never serialize or deserialize this variant." For serialization specifically, the generated code explicitly returns _serde::private::Err(_serde::ser::Error::custom("the enum variant {type}::{variant} cannot be serialized")) when that variant is encountered, as shown in serde_derive source code and confirmed in multiple GitHub issues discussing this behavior.
Citations:
- 1: https://serde.rs/variant-attrs.html
- 2: https://doc.servo.org/src/serde_derive/ser.rs.html
- 3: Binary serialize/deserialize inconsistency of enums with skipped fields serde-rs/serde#2614
- 4: Allow skipping an enum variant serde-rs/serde#2062
#[serde(skip)] on enum variants causes serialization to fail with an error.
Applying #[serde(skip)] to Event::Notification (line 420) and Event::RawNode (line 473) makes serializing these variants return an error instead of skipping fields. Since Event derives Serialize, this breaks the serialization contract for callers attempting to serialize these variants. Use a custom serialization strategy if the intent is to keep Event: Serialize while excluding the Arc<OwnedNodeRef> data.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/types/events.rs` around lines 420 - 421, Remove the #[serde(skip)]
attribute from the Event enum variants and implement a custom Serialize for the
enum (impl Serialize for Event) that matches the derived variant naming but
omits/serializes placeholder data for the Notification and RawNode variants
(i.e., when matching Event::Notification(Arc<OwnedNodeRef>) and
Event::RawNode(Arc<OwnedNodeRef>) serialize them as unit variants or with
minimal marker fields instead of attempting to serialize the Arc<OwnedNodeRef>),
so Event can still implement Serialize while excluding the OwnedNodeRef payload.
fafb553 to
4469c2e
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
wacore/src/message_processing.rs (1)
113-119: 🧹 Nitpick | 🔵 TrivialConsider using consistent attribute access pattern.
Lines 114 and 168 use direct field access (
enc_node.attrs.get(...)), while lines 106, 121, and 137 use the method call (enc_node.attrs().optional_*()). For consistency and to leverage the typed helper methods, consider refactoring lines 113-119:♻️ Proposed refactor for consistency
- if enc_node - .attrs - .get("decrypt-fail") - .is_some_and(|v| v == "hide") - { - has_hide_fail = true; - } + if enc_node + .attrs() + .optional_string("decrypt-fail") + .is_some_and(|s| s.as_ref() == "hide") + { + has_hide_fail = true; + }Similarly, consider updating line 168 to use
attrs()if a helper method is available.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/message_processing.rs` around lines 113 - 119, Replace the direct field access enc_node.attrs.get("decrypt-fail") used when setting has_hide_fail with the same attrs() helper pattern used elsewhere (e.g., enc_node.attrs().optional_*()), e.g., call the typed helper used at lines 106/121/137 to fetch the "decrypt-fail" attribute and test for "hide"; also update the similar access at line 168 to use attrs() for consistency so both checks use the same helper methods rather than direct .attrs field access.src/handlers/message.rs (1)
84-99: 🧹 Nitpick | 🔵 TrivialRemove the per-message
Box::pinallocation in the worker loop.The future is awaited immediately, so
Box::pin(...)only adds a heap allocation on this hot path. Awaitclient.handle_incoming_message(msg_node)directly.♻️ Minimal change
- Box::pin(client.handle_incoming_message(msg_node)).await; + client.handle_incoming_message(msg_node).await;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/message.rs` around lines 84 - 99, The worker loop currently wraps the per-message future in Box::pin before awaiting, causing an unnecessary heap allocation: inside the spawned async block that receives from rx (the while let Ok(msg_node) = rx.recv().await loop) remove the Box::pin(...) wrapper and simply await the future returned by client_for_worker.clone().handle_incoming_message(msg_node) directly; keep the surrounding logic (connection_generation check, timing with wacore::time::now_millis) unchanged so only the Box::pin allocation is eliminated.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorPropagate malformed
<host>nodes instead of dropping them.This
filter_map(...ok()?)turns host parse failures into missing hosts. A bad<host>stanza should fail the IQ parse, not silently shrink the retry list.🐛 Suggested fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(|host_node| { + let ext = MediaConnHostExtended::try_from_node_ref(host_node)?; + Ok(MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<Vec<_>, anyhow::Error>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The code currently uses filter_map(...ok()?) when parsing hosts which swallows parse errors from MediaConnHostExtended::try_from_node_ref and silently drops malformed <host> nodes; change the parsing to map each host_node to a Result<MediaConnHost, _> by calling MediaConnHostExtended::try_from_node_ref(host_node).map(|ext| MediaConnHost { hostname: ext.hostname, host_type: ext.host_type, fallback_hostname: ext.fallback_hostname }) and then collect::<Result<Vec<MediaConnHost>, _>>() (or otherwise propagate the first error) so parse failures from MediaConnHostExtended bubble up from the enclosing IQ parse function instead of being filtered out.wacore/src/request.rs (1)
201-226: 🧹 Nitpick | 🔵 TrivialDrop the boxed
Resulton the IQ parse hot path.
parse_iq_response()still allocates aBoxfor every response even though the function can returnResult<(), IqError>directly. That undercuts the allocation reductions this PR is aiming for.♻️ Suggested cleanup
- pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Box<Result<(), IqError>> { + pub fn parse_iq_response(&self, response_node: &NodeRef<'_>) -> Result<(), IqError> { if response_node.tag == "stream:error" || response_node.tag == "xmlstreamend" { - return Box::new(Err(IqError::Disconnected(response_node.to_owned()))); + return Err(IqError::Disconnected(response_node.to_owned())); } if let Some(res_type) = response_node.get_attr("type") && res_type.as_str() == "error" { let error_child = response_node.get_optional_child_by_tag(&["error"]); if let Some(error_node) = error_child { let mut parser = error_node.attrs(); let code = parser.optional_u64("code").unwrap_or(0) as u16; let text = parser .optional_string("text") .as_deref() .unwrap_or("") .to_string(); - return Box::new(Err(IqError::ServerError { code, text })); + return Err(IqError::ServerError { code, text }); } - return Box::new(Err(IqError::ServerError { + return Err(IqError::ServerError { code: 0, text: "Malformed error response".to_string(), - })); + }); } - Box::new(Ok(())) + Ok(()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/request.rs` around lines 201 - 226, The function parse_iq_response currently returns Box<Result<(), IqError>> causing unnecessary heap allocation on the hot path; change its signature to return Result<(), IqError> and remove all Box::new(...) wrappers in its body so you return Err(IqError::...) or Ok(()) directly (preserve the same error cases: Disconnected(response_node.to_owned()), ServerError { code, text }, and the malformed error case). After this change, update any callers of parse_iq_response to handle a plain Result<(), IqError> instead of a boxed Result.src/retry.rs (1)
235-252:⚠️ Potential issue | 🟠 MajorIgnore invalid registration ID
0in the session-deletion fallback.
process_retry_key_bundle()already rejectsregistration_id == 0, but this fallback path still treats0as a mismatch and can delete an otherwise valid session after a malformed retry receipt.🐛 Proposed fix
- if let Some(received_reg_id) = extract_registration_id_from_node_ref(nr) { + if let Some(received_reg_id) = extract_registration_id_from_node_ref(nr) + && received_reg_id != 0 + { let signal_address = resolved_jid.to_protocol_address(); let device_store = self.persistence_manager.get_device_arc().await; let device_guard = device_store.read().await; @@ - if let Some(session) = session + if let Some(session) = session && let Ok(stored_reg_id) = session.remote_registration_id() && stored_reg_id != 0 && stored_reg_id != received_reg_id {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/retry.rs` around lines 235 - 252, The fallback path that compares received_reg_id to the session's stored_reg_id should ignore registration ID 0 (invalid) so we don't delete a valid session after a malformed retry; update the condition in the block using extract_registration_id_from_node_ref, signal_cache.get_session, and session.remote_registration_id() so it only treats it as a mismatch when both stored_reg_id != 0 AND received_reg_id != 0 AND stored_reg_id != received_reg_id (i.e., skip deletion if either id is 0).src/client.rs (2)
4505-4508: 🛠️ Refactor suggestion | 🟠 MajorUse
DeviceCommandfor test setup too.These setups bypass
PersistenceManager::process_command()and mutate device state directly, so they no longer exercise the same state-transition path production uses. Please seed PN throughDeviceCommandhere as well. As per coding guidelines, "Never modify Device state directly; useDeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()".Also applies to: 4587-4590
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4505 - 4508, The test is mutating device state directly via pm.modify_device(...) which bypasses PersistenceManager::process_command(); instead construct the appropriate DeviceCommand variant that sets the device PN (e.g., an UpdatePhoneNumber/SetPn variant) and submit it to pm.process_command(...) to seed the PN, then read back via pm.get_device_snapshot() to verify; replace the direct pm.modify_device(...) calls at both locations with creating the DeviceCommand, awaiting pm.process_command(command).await, and using get_device_snapshot() to assert the PN.
2492-2555: 🧹 Nitpick | 🔵 TrivialAvoid parsing the same app-state response twice.
Both sync paths first call
parse_patch_list*_ref(resp.get())to discover external blobs and then immediately calldecode_*_ref(resp.get(), ...)on the same tree. On large startup sync payloads, that doubles traversal and attribute decoding on a latency-sensitive path. Consider a single decode API that also exposes the external blob refs, or thread the parsed list into the processor.Also applies to: 2712-2772
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 2492 - 2555, The code currently calls wacore::appstate::patch_decode::parse_patch_lists_ref(resp.get()) to discover external blobs and then calls proc.decode_multi_patch_list_ref(resp.get(), &download, true) which re-traverses and re-decodes the same app-state tree; to fix, change flow so the parsed patch lists are produced once and reused: either modify decode_multi_patch_list_ref (and related decode_*_ref APIs) to accept parsed patch lists (or return external refs along with decode results), or thread the parsed patch_lists into get_app_state_processor()/decode_multi_patch_list_ref so the processor uses the already-parsed structure instead of calling parse_patch_lists_ref again; update the pre_downloaded/download logic to consume the single parsed patch_lists and remove the duplicate parse of resp.get().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@wacore/src/iq/groups.rs`:
- Around line 590-601: The code is still extracting JIDs via string + parse
(attrs.optional_string(...).parse::<Jid>()), reintroducing extra work from the
NodeRef migration; replace those with the NodeRef/JID-aware accessors (e.g.,
call the attrs method that returns an Option<Jid> directly) for the creator and
subject_owner fields: replace the creator initialization and the subject_owner
initialization that use attrs.optional_string(...) and parse::<Jid>() with the
appropriate attrs.optional_jid("creator") and attrs.optional_jid("s_o") (or the
project’s equivalent JID-returning accessor) and remove the manual parse steps
so the variables remain Option<Jid>.
---
Outside diff comments:
In `@src/client.rs`:
- Around line 4505-4508: The test is mutating device state directly via
pm.modify_device(...) which bypasses PersistenceManager::process_command();
instead construct the appropriate DeviceCommand variant that sets the device PN
(e.g., an UpdatePhoneNumber/SetPn variant) and submit it to
pm.process_command(...) to seed the PN, then read back via
pm.get_device_snapshot() to verify; replace the direct pm.modify_device(...)
calls at both locations with creating the DeviceCommand, awaiting
pm.process_command(command).await, and using get_device_snapshot() to assert the
PN.
- Around line 2492-2555: The code currently calls
wacore::appstate::patch_decode::parse_patch_lists_ref(resp.get()) to discover
external blobs and then calls proc.decode_multi_patch_list_ref(resp.get(),
&download, true) which re-traverses and re-decodes the same app-state tree; to
fix, change flow so the parsed patch lists are produced once and reused: either
modify decode_multi_patch_list_ref (and related decode_*_ref APIs) to accept
parsed patch lists (or return external refs along with decode results), or
thread the parsed patch_lists into
get_app_state_processor()/decode_multi_patch_list_ref so the processor uses the
already-parsed structure instead of calling parse_patch_lists_ref again; update
the pre_downloaded/download logic to consume the single parsed patch_lists and
remove the duplicate parse of resp.get().
In `@src/handlers/message.rs`:
- Around line 84-99: The worker loop currently wraps the per-message future in
Box::pin before awaiting, causing an unnecessary heap allocation: inside the
spawned async block that receives from rx (the while let Ok(msg_node) =
rx.recv().await loop) remove the Box::pin(...) wrapper and simply await the
future returned by client_for_worker.clone().handle_incoming_message(msg_node)
directly; keep the surrounding logic (connection_generation check, timing with
wacore::time::now_millis) unchanged so only the Box::pin allocation is
eliminated.
In `@src/retry.rs`:
- Around line 235-252: The fallback path that compares received_reg_id to the
session's stored_reg_id should ignore registration ID 0 (invalid) so we don't
delete a valid session after a malformed retry; update the condition in the
block using extract_registration_id_from_node_ref, signal_cache.get_session, and
session.remote_registration_id() so it only treats it as a mismatch when both
stored_reg_id != 0 AND received_reg_id != 0 AND stored_reg_id != received_reg_id
(i.e., skip deletion if either id is 0).
In `@wacore/src/iq/mediaconn.rs`:
- Around line 375-385: The code currently uses filter_map(...ok()?) when parsing
hosts which swallows parse errors from MediaConnHostExtended::try_from_node_ref
and silently drops malformed <host> nodes; change the parsing to map each
host_node to a Result<MediaConnHost, _> by calling
MediaConnHostExtended::try_from_node_ref(host_node).map(|ext| MediaConnHost {
hostname: ext.hostname, host_type: ext.host_type, fallback_hostname:
ext.fallback_hostname }) and then collect::<Result<Vec<MediaConnHost>, _>>() (or
otherwise propagate the first error) so parse failures from
MediaConnHostExtended bubble up from the enclosing IQ parse function instead of
being filtered out.
In `@wacore/src/message_processing.rs`:
- Around line 113-119: Replace the direct field access
enc_node.attrs.get("decrypt-fail") used when setting has_hide_fail with the same
attrs() helper pattern used elsewhere (e.g., enc_node.attrs().optional_*()),
e.g., call the typed helper used at lines 106/121/137 to fetch the
"decrypt-fail" attribute and test for "hide"; also update the similar access at
line 168 to use attrs() for consistency so both checks use the same helper
methods rather than direct .attrs field access.
In `@wacore/src/request.rs`:
- Around line 201-226: The function parse_iq_response currently returns
Box<Result<(), IqError>> causing unnecessary heap allocation on the hot path;
change its signature to return Result<(), IqError> and remove all Box::new(...)
wrappers in its body so you return Err(IqError::...) or Ok(()) directly
(preserve the same error cases: Disconnected(response_node.to_owned()),
ServerError { code, text }, and the malformed error case). After this change,
update any callers of parse_iq_response to handle a plain Result<(), IqError>
instead of a boxed Result.
---
Duplicate comments:
In `@src/client.rs`:
- Around line 3638-3652: The ACK path in build_ack_node rebuilds JID-backed
attributes (from, participant, type) by calling get_attr(...).as_str() and
re-wrapping into NodeValue, defeating the zero-copy intent; change the code to
preserve and carry the original typed attribute values instead of converting to
strings—e.g., use the wacore_binary typed attribute accessor (the
typed/JID-returning get_attr variant or the raw attribute value) and construct
NodeValue directly from that typed value for the fields from, participant and
type so no as_str/as_ref reallocation occurs; update build_ack_node to use those
typed accessors and NodeValue constructors for those three attributes.
- Around line 4900-4902: The local shim function fn node_to_owned_ref(node:
Node) -> Arc<wacore_binary::OwnedNodeRef> simply forwards to
crate::test_utils::node_to_owned_ref and should be removed; instead add a use
crate::test_utils::node_to_owned_ref; (or a renamed import if a name conflict
exists) and update all local call sites to call the imported
crate::test_utils::node_to_owned_ref directly, then delete the wrapper function
node_to_owned_ref to avoid the redundant indirection.
- Around line 2299-2317: The code removes the waiter from self.response_waiters
before attempting the fallible marshal_ref/OwnedNodeRef::new conversion, so if
conversion fails the waiter is dropped instead of being fulfilled; change the
logic to materialize the ACK payload first (call
wacore_binary::marshal::marshal_ref and OwnedNodeRef::new) and only after
successful construction remove the waiter from response_waiters and send the
Arc<OwnedNodeRef>, or if materialization fails explicitly notify the waiter of
failure (e.g., send an error outcome) instead of just dropping it; locate the
logic around id_opt, response_waiters.lock().await.remove(&id), marshal_ref, and
OwnedNodeRef::new and reorder/adjust to keep the waiter alive until send or
explicit failure is performed.
In `@src/client/device_registry.rs`:
- Around line 301-313: The current delete_sessions_for_devices function
cross-products lookup.all_keys() with both Server::Lid and Server::Pn; instead
match on the resolved UserLookupKeys from resolve_lookup_keys() and only purge
the valid (user, server) pairs: handle UserLookupKeys::LidWithPn and PnWithLid
by purging lid with Server::Lid and pn with Server::Pn, and handle Unknown by
purging the caller's actual server only (so thread a caller_server parameter
through clear_device_record()/patch_device_remove() into
delete_sessions_for_devices); create a small purge helper used by
delete_sessions_for_devices to convert a (user, server, device_ids) into the
Jid/address and call self.signal_cache.delete_session for each device.
In `@src/handlers/ib.rs`:
- Around line 113-116: Collapse the nested `if let` checks into a single
let-chain: replace the nested `if let Some(routing_info_node) =
child.get_optional_child("routing_info") { if let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref() { ... } }` with a single `if let
Some(routing_info_node) = child.get_optional_child("routing_info") && let
Some(NodeContentRef::Bytes(routing_bytes)) =
routing_info_node.content.as_deref() { ... }` pattern so extraction of
`routing_info_node` and `routing_bytes` uses a let-chain; keep the same body
logic and references to `routing_bytes` and `NodeContentRef::Bytes`.
In `@src/handlers/notification.rs`:
- Around line 555-558: The closure building AccountSyncDevice currently maps
optional_u64("key-index") with a silent truncating cast (map(|v| v as u32));
change this to reject overflow by using u32::try_from(v).ok() (or
v.try_into().ok()) so that overflowing key-index values yield None; update the
closure around optional_u64("key-index") and ensure TryFrom/TryInto is in scope
if needed; keep the produced type as Option<u32> to populate
AccountSyncDevice.key_index.
In `@src/message.rs`:
- Around line 376-383: The code is forcing a to_owned() on enc nodes
(enc_node.to_owned()/enc_type.to_string()) before dispatching custom handlers,
breaking zero-copy; change the handler boundary to accept references (e.g.,
&NodeRef or &OwnedNodeRef) instead of owned Node and string so you can pass
enc_node and enc_type by reference directly in the runtime.spawn closure (remove
enc_node_owned/enc_type_owned), update the handler trait/signature
(handler_clone.handle) to take &NodeRef/&OwnedNodeRef and &str/&Cow<str> as
appropriate, and only clone/own inside individual handler implementations when
they truly need ownership. Ensure all call sites and trait impls are updated to
the new reference types.
In `@src/pair_code.rs`:
- Around line 238-245: The code currently converts the 80-byte
NodeContentRef::Bytes to a Vec<u8> (primary_wrapped_ephemeral) causing an
allocation; change the branch to produce a [u8; 80] instead. Update the match
arm that checks Some(NodeContentRef::Bytes(b)) if b.len() == 80 to convert b
into a fixed-size array (e.g., b.try_into() or by copying with copy_from_slice)
and return that [u8; 80]; ensure the variable primary_wrapped_ephemeral’s type
is changed accordingly so downstream code uses the stack-allocated array.
In `@src/pair.rs`:
- Around line 195-203: The code currently uses
optional_jid(...).unwrap_or_default() for parsed_jid and parsed_lid which
silently accepts missing or malformed JIDs; change this so pair-success is
rejected instead: when extracting from success_node -> device_node, parse the
attributes with the existing parser but treat jid and lid as required (fail the
handler/return an Err) if parser.optional_jid("jid") or
parser.optional_jid("lid") yields None or a parse error; do not default to
Jid::default(), and ensure the downstream SetId/SetLid logic only runs when both
parsed_jid and parsed_lid are successfully obtained (mirroring the strict
validation used for device-identity).
In `@src/retry.rs`:
- Around line 532-567: process_retry_key_bundle is redundantly calling
resolve_encryption_jid on requester_jid even though handle_retry_receipt already
resolves the participant; update the API to avoid the extra lookup by changing
process_retry_key_bundle to accept the already-resolved identity (e.g., add a
parameter like resolved_jid: &ResolvedJid or signal_address: &str) and remove
the call to self.resolve_encryption_jid(requester_jid). Locate the resolve call
and the use of signal_address in process_retry_key_bundle, update all call sites
(including handle_retry_receipt) to pass the resolved value, and ensure types
match (use to_protocol_address() at the caller if you pass a string) so no extra
lookup occurs.
In `@tests/e2e/tests/memory_soak.rs`:
- Line 409: Extract the repeated predicate into a single helper function (e.g.,
is_gp2_notification) that accepts the Event (or the node) and returns a bool by
checking matches!(e, Event::Notification(node) if
node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")); replace the three
inline occurrences with calls to this helper so all references use the same
logic; update any test imports/visibility so memory_soak.rs can call
is_gp2_notification and run the tests to confirm behavior is unchanged.
In `@tests/e2e/tests/offline_groups.rs`:
- Line 147: Extract the inline `w:gp2` check into a shared predicate and use it
in both places (the current inline match using `Event::Notification(node) if
node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")` and the other
occurrence around line 436). Implement a helper (e.g., `fn
is_group_notification(node: &Node) -> bool` or `fn is_gp2_notification(event:
&Event) -> bool`) that encapsulates `get_attr("type").is_some_and(|v| v.as_str()
== "w:gp2")`, and replace the inline match guards with calls to that helper (for
example `matches!(e, Event::Notification(node) if is_group_notification(node))`)
so the logic is centralized and reused.
In `@wacore/appstate/src/patch_decode.rs`:
- Around line 89-99: The parse_patch_list_ref and parse_patch_lists_ref
functions currently call node.to_owned() and therefore allocate; change the code
so the borrowed NodeRef is threaded end-to-end into parsing helpers: introduce
internal functions (e.g., parse_patch_list_from_ref and
parse_patch_lists_from_ref or make parse_single_collection accept &NodeRef<'_>)
that operate on &NodeRef<'_> without cloning, move the traversal/parse logic
(including parse_single_collection/path descent) into those &NodeRef-taking
helpers, and have parse_patch_list and parse_patch_lists call these helpers or
thinly wrap them so the _ref entry points no longer call to_owned().
In `@wacore/binary/src/decoder.rs`:
- Around line 136-143: The match on the incoming AD_JID "agent" byte in
decoder.rs currently maps unknown values to Server::Pn; instead update the match
in the agent decoding logic so that only the explicit cases (0 => Server::Pn, 1
=> Server::Lid, 128 => Server::Hosted, 129 => Server::HostedLid, and the (n &
128)!=0 && (n & 1)==0 rule) return servers and all other values return a decode
error (do not coerce to Server::Pn). Locate the match on `agent` in the decoder
(the arm producing `server`) and replace the wildcard `_ =>
crate::jid::Server::Pn` with code that fails fast by returning or propagating an
appropriate decode error (create or reuse a DecodeError variant that includes
the offending `agent` byte).
In `@wacore/binary/src/jid.rs`:
- Around line 508-512: The actual_agent() method only zeroes the agent for
Server::Pn but should treat all agent-less server variants the same; update
jid::actual_agent() to return 0 when self.server is Server::Pn, Server::Lid,
Server::Hosted, or Server::HostedLid and return self.agent for all other servers
so its behavior matches Display and is_ad() and avoids leaking hidden agent
state for manually constructed JIDs.
- Around line 624-631: The fast-path parser parse_jid_fast currently coerces
malformed numeric agent/device parts to 0 and causes the early return in the Jid
construction to bypass stricter validation; update parse_jid_fast so it returns
None whenever a numeric component is present but contains non-digit characters
or is out of allowed range (e.g., >65535 or otherwise invalid), ensuring the
caller (code that does if let Some(parts) = parse_jid_fast(s) { ... return
Ok(Jid { ... }) }) falls back to the full parser and triggers the proper
validation; apply the same change to the other fast-path usage locations that
build Jid from parse_jid_fast results so no malformed numeric component is
silently coerced.
In `@wacore/binary/src/node.rs`:
- Around line 663-679: OwnedNodeRef is missing a forwarding helper for
content_as_string(), causing callers to call .get() manually; add a new
#[inline] pub fn content_as_string(&self) -> Option<String> (or matching return
type exposed on NodeRef) on OwnedNodeRef that simply calls
self.get().content_as_string() to mirror NodeRef's API and restore parity
between OwnedNodeRef and NodeRef.
- Around line 589-593: Update the documentation for OwnedNodeRef to remove the
overly strong “zero allocation beyond the buffer itself” claim: state that
OwnedNodeRef avoids copying string/byte payloads out of the decode buffer but
that the inner NodeRef still allocates containers for attributes and child lists
(so there are container allocations). Edit the doc comment above the
OwnedNodeRef struct (and adjust any inline comments on NodeRef if present) to
reflect this accurate allocation behavior and use the symbols OwnedNodeRef and
NodeRef so readers can locate the types.
In `@wacore/derive/src/lib.rs`:
- Around line 185-194: The generated code for AttrType::Jid currently calls
node.attrs().optional_jid(`#attr_name`) directly and never calls the attr-parser's
finish(), which swallows parsing failures; update the try_from_node_ref
generation so both optional and required JID branches create a single parser
variable (e.g. let mut attr_parser = node.attrs();), call
attr_parser.optional_jid(`#attr_name`) or attr_parser.jid/#attr_name accordingly,
then after collecting all fields call attr_parser.finish() and propagate its
error (for required JIDs keep the existing ok_or_else for missing attribute but
return parse errors from finish() instead of masking them); apply the same
change for the other JID-generation site (the similar block around AttrType::Jid
at the second occurrence).
In `@wacore/src/appstate_sync.rs`:
- Around line 98-156: decode_patch_list_ref duplicates the snapshot +
external-mutations download/hydration logic found in decode_patch_list; extract
that shared flow into a single helper (e.g., hydrate_patch_list) and call it
from both entry points. Implement a helper with signature something like fn
hydrate_patch_list<FDownload>(pl: &mut PatchList, download: FDownload) ->
Result<(), Error> where FDownload: Fn(&wa::ExternalBlobReference) ->
Result<Vec<u8>> + Send + Sync; move the snapshot-ref handling
(parse_patch_list_ref result mutation) and the loop that downloads/decodes
external_mutations (with the same logging and patch.mutations replacement) into
it, then have decode_patch_list_ref and decode_patch_list call parse_* to obtain
a PatchList, call hydrate_patch_list(&mut pl, download) and finally call
self.process_patch_list(pl, validate_macs). Ensure signatures and Send+Sync
bounds match the original download closure usage.
In `@wacore/src/iq/blocklist.rs`:
- Around line 116-131: Replace the duplicated parsing logic in parse_response
with a call to BlocklistResponse::try_from_node_ref() so the existing traversal
of optional <list> vs direct <item> and the shared warn-on-parse-error behavior
is reused; locate the parse_response method in blocklist.rs and have it delegate
to BlocklistResponse::try_from_node_ref(response) (and return or map its Result
appropriately) instead of reimplementing the item collection/filter_map logic.
In `@wacore/src/iq/business.rs`:
- Around line 55-61: The node_text function unnecessarily clones bytes with
b.to_vec() before UTF-8 validation; instead validate the borrowed slice directly
and allocate only once by using std::str::from_utf8 on the borrowed bytes
(NodeContentRef::Bytes(b)) and then map the &str to String (e.g.,
from_utf8(b).map(|s| s.to_string()).ok()), leaving the match arms for
NodeContentRef::String and the fallback unchanged.
In `@wacore/src/iq/chatstate.rs`:
- Around line 140-153: optional_jid() conflates "missing" and "malformed" by
returning None for both; update the from and participant handling to check
attribute presence first and fail on parse errors instead of falling through to
the happy path: call attrs.get("from") (or check attrs.has("from")) before using
attrs.optional_jid("from") and if the attribute exists but optional_jid returned
None, return a parse error (create/use a ChatstateParseError::MalformedFrom or a
generic malformed-jid variant) instead of emitting MissingFrom or proceeding; do
the same for the "participant" attribute so that
attrs.optional_jid("participant") returning None when the attribute was present
yields an error rather than silently constructing a ChatstateSource::Group vs
1:1.
In `@wacore/src/iq/groups.rs`:
- Around line 379-382: The current parsing uses
attrs.optional_string("type").and_then(|s|
ParticipantType::try_from(s.as_ref()).ok()).unwrap_or(ParticipantType::Member)
which silently converts unknown/invalid participant type strings into
ParticipantType::Member; change this so an invalid/non-matching string causes
the parse to fail instead of defaulting: attempt to convert with
ParticipantType::try_from and if it returns Err propagate/return a parse error
(or use ? to bubble the error) rather than falling back to Member, so that
unknown types are not coerced and admin/superadmin state is preserved.
In `@wacore/src/iq/keepalive.rs`:
- Around line 61-69: The test test_keepalive_spec_build_iq should explicitly
assert the keepalive destination to lock the wire contract; after creating the
iq via KeepaliveSpec::new().build_iq() add an assertion that iq.to equals the
expected JID for the server (e.g. Jid::new("s.whatsapp.net", Server::Pn) or the
equivalent string) so that any future change to Jid mapping fails the test;
reference KeepaliveSpec::new, build_iq, test_keepalive_spec_build_iq, Jid::new
and Server::Pn when adding this assertion.
In `@wacore/src/iq/mediaconn.rs`:
- Around line 303-317: The attr parser in try_from_node_ref is not finalized so
numeric parse failures are swallowed; after reading ttl, auth_ttl, max_buckets,
set_ip_token (and other optional_* calls) call attrs.finish() and propagate any
error so malformed numeric attributes cause the IQ parse to fail rather than
silently returning 0/None; update the function to invoke attrs.finish() (and
return its Err wrapped via anyhow! or ? as appropriate) before constructing and
returning the MediaConn.
In `@wacore/src/iq/node.rs`:
- Around line 22-26: The helper required_attr currently eagerly allocates by
calling v.to_string(); change it to return Result<std::borrow::Cow<'_, str>,
anyhow::Error> and map the Option from node.get_attr(key) into a Cow so borrowed
slices are preserved when possible (e.g., map(|v| Cow::Borrowed(v)) or
Cow::from(v) that yields borrowed for &str and owned when necessary), keeping
the same error on missing key; update callers if needed to accept Cow or call
into_owned() where an owned String is required.
In `@wacore/src/iq/usync.rs`:
- Around line 122-128: The parse_lid_jid helper (and the other sites noted)
currently calls attrs().optional_string("val") then parses into Jid, causing an
allocation and reparse; replace those with attrs().optional_jid("val") so the
decoder returns a ValueRef::Jid zero-copy Jid directly. Locate usages in
parse_lid_jid and the similar handlers around the other ranges (lines
referenced) that call attrs().optional_string("val") and swap them to
attrs().optional_jid("val") while preserving the same Option<Jid> return/flow.
In `@wacore/src/media_retry.rs`:
- Around line 178-183: The code allocates by calling into_owned() on the
attribute string; keep it borrowed to avoid allocation in the zero-copy parser
by removing into_owned() and using a &str (or Cow<'_, str> if necessary) for
msg_id; update usages that expect an owned String to accept &str (e.g., the
variable msg_id created from node.get_attr("id") and any downstream calls in the
decryption and stanza-id validation logic) so the id is validated and used
without cloning.
- Around line 97-107: The function decrypt_media_retry_notification currently
calls Nonce::from_slice(iv) which will panic if iv is not 12 bytes; validate the
iv length before constructing the nonce and return a handled error instead of
allowing a panic. In decrypt_media_retry_notification, after
derive_media_retry_key and before Aes256Gcm/Nonce usage, check iv.len() == 12
and if not return Err(anyhow!("invalid IV length: expected 12 bytes, got {}",
iv.len())); then safely call Nonce::from_slice(iv) and proceed with decryption
using Aes256Gcm.
In `@wacore/src/pair.rs`:
- Around line 85-98: build_ack_node_ref duplicates the ACK IQ construction from
build_ack_node; refactor by extracting the shared IQ-building logic into a
single helper (e.g., a private fn build_ack_iq(to: &str, id: &str) -> Node or
similar) and have both build_ack_node_ref and build_ack_node call that helper;
update build_ack_node_ref to extract "to" and "id" from the NodeRef (using
NodeRef::get_attr) and pass them to the new helper instead of constructing a
NodeBuilder locally (referencing build_ack_node, build_ack_node_ref,
NodeBuilder, and NodeRef to locate code).
In `@wacore/src/prekeys.rs`:
- Around line 144-149: The helper extract_bytes_ref currently clones bytes into
a Vec<u8> then callers copy into fixed-size arrays; change it to return a
borrowed slice (Result<&[u8], anyhow::Error>) so callers can do zero-copy reads
and only copy into arrays at the final boundary. Update the signature of
extract_bytes_ref (currently fn extract_bytes_ref(node: Option<&NodeRef<'_>>) ->
Result<Vec<u8>, anyhow::Error>) to return a lifetime-linked &[u8] tied to the
NodeRef input, adjust the match arm for NodeContentRef::Bytes to return the
slice reference, and update all call sites (e.g., places extracting registration
ID) to accept a &[u8] and perform the fixed-size array copy there.
In `@wacore/src/stanza/devices.rs`:
- Around line 317-321: The code currently sets stanza_id using
node.get_attr("id").map(|v| v.as_str()).unwrap_or_default().into_owned(), which
converts missing or malformed ids into an empty string; change this to treat the
id as required: replace the unwrap_or_default usage with an explicit match or
if-let that returns an early error/skip when node.get_attr("id") is None or
invalid (e.g., return Err(...) or continue from the surrounding parsing
function), using the same surrounding function's error/flow handling so
ACK/dedup logic never sees a synthetic empty stanza_id; reference symbols:
stanza_id and node.get_attr("id") to locate the change.
In `@wacore/src/types/events.rs`:
- Around line 420-421: The Event enum currently derives Serialize while variants
Notification(Arc<OwnedNodeRef>) and RawNode(Arc<OwnedNodeRef>) are annotated
with #[serde(skip)], which makes serializing those variants fail at runtime;
either remove Serialize from Event or implement a custom Serialize for Event
that handles those two variants explicitly (e.g., match on Event in impl
Serialize for Event and for Notification and RawNode emit a placeholder/unit
form or a lightweight tag instead of trying to serialize Arc<OwnedNodeRef>,
while delegating normal serialization to the other variants). Locate the Event
enum and modify the derives or add an impl Serialize that matches on Event and
serializes Notification and RawNode as unit/placeholder values to avoid runtime
errors.
In `@wacore/src/usync.rs`:
- Around line 153-154: The function parse_lid_mappings_from_response currently
accepts &NodeRef<'_> which is inconsistent with the sibling APIs; change its
signature to accept &Node (matching parse_get_user_devices_response_with_phash
and parse_get_user_devices_response) and update any internal uses to obtain a
NodeRef via Node::as_ref or similar only inside the function so external callers
keep using &Node; ensure the function name parse_lid_mappings_from_response
remains unchanged and adjust imports/types inside the body to compile with
&Node.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 49522472-13f8-441d-9415-cdf5d1e45832
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (120)
.gitignoreCargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/benches/binary_benchmark.rswacore/binary/src/attrs.rswacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rswacore/tests/binary_protocol_test.rs
d74ee95 to
c035344
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
wacore/src/types/jid.rs (1)
5-8: 🧹 Nitpick | 🔵 TrivialConsider moving the mapping logic into the
Serverenum.Since
Serveris now a typed enum with a fixed set of variants, the "s.whatsapp.net" → "c.us" mapping could be implemented as a method onServer(e.g.,Server::as_signal_str()) rather than performing a runtime string comparison. This would eliminate the branch and make the mapping intent clearer.♻️ Proposed refactor
Add a method to the
Serverenum inwacore_binary:// In wacore_binary's Server enum implementation impl Server { pub fn as_signal_str(&self) -> &'static str { match self { Server::Pn => "c.us", // s.whatsapp.net mapped to c.us Server::Lid => "lid", Server::Group => "g.us", // ... other variants return their canonical form } } }Then update this file:
-#[inline] -fn mapped_server(s: &str) -> &str { - if s == "s.whatsapp.net" { "c.us" } else { s } -} - pub fn write_protocol_address_to(jid: &Jid, buf: &mut String) { use std::fmt::Write; buf.clear(); - let server = mapped_server(jid.server.as_str()); + let server = jid.server.as_signal_str(); // ... }Apply similar changes to lines 27-28 and 72.
Would you like me to generate a verification script to confirm all call sites of
mapped_serveror check if this helper is used elsewhere in the codebase?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/types/jid.rs` around lines 5 - 8, Replace the ad-hoc mapped_server function with a method on the Server enum: add a pub fn as_signal_str(&self) -> &'static str in the Server impl that returns "c.us" for Server::Pn and the canonical strings for other variants; then remove mapped_server and update all call sites that currently call mapped_server(s) to call server.as_signal_str() (or Server::as_signal_str(&server)) — specifically update the places in this file that previously compared to "s.whatsapp.net" and any usages noted in the review to use the new Server::as_signal_str method so the mapping is type-driven rather than a runtime string comparison.src/types/enc_handler.rs (1)
5-20:⚠️ Potential issue | 🟠 MajorChange
EncHandler::handlesignature to accept a zero-copy reference type.The dispatcher converts from
NodeRefto ownedNodeat line 377 ofsrc/message.rs(let enc_node_owned = (*enc_node).to_owned()) before calling the handler, which negates the zero-copy benefit. The trait definition at line 9 requires&Nodeinstead of a borrowable reference. Consider accepting&NodeRef<'_>or&OwnedNodeRefto eliminate this forced materialization while maintaining a stable public API.Evidence: conversion at dispatch site
// src/message.rs:376-377 // Custom enc handlers take &Node (public API); convert from NodeRef here. let enc_node_owned = (*enc_node).to_owned();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/types/enc_handler.rs` around lines 5 - 20, Change the EncHandler::handle signature from taking &Node to a zero-copy borrowable type (e.g. &NodeRef<'_> or &OwnedNodeRef) and update the dispatcher that currently does let enc_node_owned = (*enc_node).to_owned() so it passes the NodeRef directly into handle; adjust the trait declaration (EncHandler::handle) and all implementations to accept the new borrow type and update any callers to avoid forcing .to_owned(), ensuring async_trait bounds and Send+Sync remain satisfied for the trait object.src/client/lid_pn.rs (1)
142-161: 🛠️ Refactor suggestion | 🟠 MajorUse let-chains instead of nested
if letin the PN branch.This nested
if letviolates the repository's collapsible-if pattern guideline. Per AGENTS.md: "Always use let-chains (if let Some(x) = foo && let Some(y) = x.bar { ... }) instead of nestedif letblocks. Clippy'scollapsible_iflint will reject the nested form."♻️ Proposed refactor
- } else if target.is_pn() { - // PN JID - check if we have a LID mapping - if let Some(lid_user) = self.lid_pn_cache.get_current_lid(&target.user).await { + } else if target.is_pn() + && let Some(lid_user) = self.lid_pn_cache.get_current_lid(&target.user).await + { let lid_jid = Jid { user: lid_user.into(), server: wacore_binary::Server::Lid, device: target.device, agent: target.agent, integrator: target.integrator, }; debug!( "[SEND-LOCK] Resolved {} to LID {} for session lock", target, lid_jid ); lid_jid - } else { - // No LID mapping - use PN as-is - debug!("[SEND-LOCK] No LID mapping for {}, using PN", target); - target.clone() - } + } else if target.is_pn() { + // No LID mapping - use PN as-is + debug!("[SEND-LOCK] No LID mapping for {}, using PN", target); + target.clone() } else { // Other server type - use as-is target.clone() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/lid_pn.rs` around lines 142 - 161, Replace the nested `if let` by evaluating the async lookup first, then using a let-chain combining the PN check and the optional result; e.g. call `let maybe_lid_user = self.lid_pn_cache.get_current_lid(&target.user).await;` and then use `if target.is_pn() && let Some(lid_user) = maybe_lid_user { ... }` to build the `Jid` (symbols: target.is_pn(), self.lid_pn_cache.get_current_lid, maybe_lid_user, lid_jid, Jid) so the collapsible-if pattern is satisfied and the nested `if let` is removed.src/pair.rs (1)
61-70: 🧹 Nitpick | 🔵 TrivialMove
codesinto the spawned task instead of cloning it.
codes.clone()duplicates every QR payload even thoughcodesis never read again after this point.♻️ Minimal diff
let (stop_tx, stop_rx) = async_channel::bounded::<()>(1); - let codes_clone = codes.clone(); let client_clone = client.clone(); client .runtime .spawn(Box::pin(async move { @@ - for code in codes_clone { + for code in codes {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair.rs` around lines 61 - 70, The code currently clones `codes` into `codes_clone` and iterates over it inside the spawned task; instead take ownership of `codes` into the async task to avoid duplicating payloads—remove the `codes_clone = codes.clone()` line, drop uses of `codes_clone`, and let the `async move` closure capture `codes` directly (iterate `for code in codes { ... }`) when calling `client.runtime.spawn(...)`; ensure no other code after this block reads `codes` so the move is valid.src/client.rs (3)
4505-4508: 🛠️ Refactor suggestion | 🟠 MajorUse
DeviceCommandfor test setup too.These setups bypass the same persistence path production code uses, so they can miss snapshot/cache side effects. As per coding guidelines "Never modify Device state directly; use
DeviceCommand+PersistenceManager::process_command()and read viaget_device_snapshot()".Also applies to: 4587-4590
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4505 - 4508, The test directly mutates device state via pm.modify_device(|device| { device.pn = ... }) which bypasses persistence and cache snapshot logic; instead construct an appropriate DeviceCommand to set the phone number (e.g., a SetPhoneNumber or UpdateDevice command), call PersistenceManager::process_command(command).await to apply it, and then read back the device using PersistenceManager::get_device_snapshot() to verify state; replace the pm.modify_device usages (including the other occurrence) with this command+process_command+get_device_snapshot flow so side effects and snapshots match production paths.
3191-3194: 🧹 Nitpick | 🔵 TrivialKeep the ping sender as a
Jid.
parser.jid("from")already gives you a typed JID, but this path stringifies it andbuild_pongtakesString, which adds an avoidable allocation on every server ping.♻️ Suggested change
- let pong = build_pong(from_jid.to_string(), id.as_deref()); + let pong = build_pong(&from_jid, id.as_deref());-fn build_pong(to: String, id: Option<&str>) -> wacore_binary::Node { +fn build_pong(to: &Jid, id: Option<&str>) -> wacore_binary::Node { let mut builder = NodeBuilder::new("iq").attr("to", to).attr("type", "result"); if let Some(id) = id { builder = builder.attr("id", id); } builder.build()Also applies to: 3618-3624
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 3191 - 3194, The code currently calls parser.jid("from") to get a typed JID then calls to_string() and passes that String into build_pong, causing an unnecessary allocation; change build_pong to accept a &Jid (or add an overload accepting &Jid) so you can pass the Jid reference directly (use from_jid or &from_jid) instead of stringifying, and update the callers (the ping handler around parser.jid("from") / let from_jid and the similar block at the other ping site) to pass the Jid by reference into build_pong before awaiting self.send_node(pong). Ensure build_pong’s signature and any internal uses are adjusted to work with &Jid without allocating.
1732-1736: 🧹 Nitpick | 🔵 TrivialSkip the device snapshot read for non-
messageACKs.
build_ack_nodeonly needsown_device_pnfor<message>ACKs, so the unconditional snapshot fetch here adds avoidable async work to every receipt/notification/call ACK.♻️ Suggested change
- let device_snapshot = self.persistence_manager.get_device_snapshot().await; - let ack = match build_ack_node(node, device_snapshot.pn.as_ref()) { + let own_device_pn = if node.tag == "message" { + self.persistence_manager.get_device_snapshot().await.pn + } else { + None + }; + let ack = match build_ack_node(node, own_device_pn.as_ref()) { Some(ack) => ack, None => return Ok(()), };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 1732 - 1736, Avoid an unconditional device snapshot read: first call build_ack_node(node, None) and if it returns Some(ack) use that; only if that returns None then await self.persistence_manager.get_device_snapshot() and call build_ack_node(node, device_snapshot.pn.as_ref()) and handle the None case (return Ok(())). This keeps the snapshot fetch (get_device_snapshot / device_snapshot.pn.as_ref()) only for ACKs that actually require own_device_pn (e.g., <message> ACKs) while leaving other ACK paths unchanged.wacore/src/iq/mediaconn.rs (1)
375-385:⚠️ Potential issue | 🟠 MajorDon't silently drop malformed
<host>nodes.The
ok()?insidefilter_mapconverts anyMediaConnHostExtended::try_from_node_referror into a skipped entry. A bad host then produces a “successful”MediaConnResponsewith an incomplete host list instead of failing the IQ parse.🐛 Suggested fix
- let mut hosts: Vec<MediaConnHost> = media_conn_node - .get_children_by_tag("host") - .filter_map(|host_node| { - let ext = MediaConnHostExtended::try_from_node_ref(host_node).ok()?; - Some(MediaConnHost { - hostname: ext.hostname, - host_type: ext.host_type, - fallback_hostname: ext.fallback_hostname, - }) - }) - .collect(); + let mut hosts: Vec<MediaConnHost> = media_conn_node + .get_children_by_tag("host") + .map(MediaConnHostExtended::try_from_node_ref) + .map(|result| { + result.map(|ext| MediaConnHost { + hostname: ext.hostname, + host_type: ext.host_type, + fallback_hostname: ext.fallback_hostname, + }) + }) + .collect::<Result<Vec<_>, _>>()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 375 - 385, The code currently uses filter_map with ok()? which swallows errors from MediaConnHostExtended::try_from_node_ref and yields an incomplete hosts Vec; instead change the host parsing to collect results as a Result<Vec<MediaConnHost>, E> (e.g. map each host_node to MediaConnHostExtended::try_from_node_ref and then convert to MediaConnHost) and propagate the first error up so the IQ parse fails rather than silently skipping malformed <host> nodes; update the code that builds hosts (the media_conn_node.get_children_by_tag("host") chain and the creation of MediaConnHost from MediaConnHostExtended) to return Err on any parse failure and surface that error out of the surrounding parse function.wacore/src/usync.rs (1)
163-170: 🧹 Nitpick | 🔵 TrivialUse the typed JID attr parser here instead of stringifying and reparsing.
optional_string("jid").parse()allocates and revalidates a value thatoptional_jid("jid")can already decode directly. That gives back part of the typed/zero-copy win this PR is introducing.♻️ Suggested cleanup
- let user_jid_str = match user_node.attrs().optional_string("jid") { - Some(jid) => jid, - None => continue, - }; - let user_jid: Jid = match user_jid_str.parse() { - Ok(j) => j, - Err(_) => continue, - }; + let user_jid = match user_node.attrs().optional_jid("jid") { + Some(jid) => jid, + None => continue, + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/usync.rs` around lines 163 - 170, The code is allocating and reparsing the JID string; replace the two-step optional_string("jid") + parse with the typed parser optional_jid("jid") on the attrs to avoid allocation and revalidation. In the block that defines user_jid_str and user_jid, call user_node.attrs().optional_jid("jid") (or the equivalent method on Attrs) and match on Some(jid) / None to continue, preserving the same variable name (user_jid) and the same early-continue behavior.
♻️ Duplicate comments (26)
wacore/src/pair.rs (1)
85-98: 🧹 Nitpick | 🔵 TrivialKeep the ACK builder in one place.
build_ack_node_refnow mirrorsbuild_ack_nodefield-for-field, so the next ACK-shape change can drift again. Please funnel both entry points through one private builder.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/pair.rs` around lines 85 - 98, Create a single private ACK-builder and have both public entry points call it: extract the common build logic into a private function (e.g., fn build_ack_node_impl(to: &str, id: &str) -> Node) and change build_ack_node and build_ack_node_ref to only obtain the to/id (build_ack_node already has them; build_ack_node_ref should map attributes "from" and "id") and then call build_ack_node_impl(to, id). This ensures build_ack_node_ref and build_ack_node no longer duplicate the NodeBuilder attrs and centralizes future ACK-shape changes.wacore/src/types/events.rs (1)
420-421:⚠️ Potential issue | 🟠 Major
#[serde(skip)]still makes theseEventvariants fail serialization.On enum variants, Serde treats
skipas “never serialize/deserialize,” soEvent::NotificationandEvent::RawNodewill error when serialized instead of serializing the enum without the payload. That still breaks theEvent: Serializecontract for the new zero-copy variants.Does Serde's `#[serde(skip)]` on an enum variant cause serializing that variant to return an error rather than omit the payload?Also applies to: 473-474
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/types/events.rs` around lines 420 - 421, The enum-level #[serde(skip)] causes Serde to error when serializing those variants; instead annotate the variant payload fields to be skipped for (de)serialization. Update the Event enum so the payloads read like Notification(#[serde(skip_serializing, skip_deserializing)] Arc<OwnedNodeRef>) and likewise for RawNode(#[serde(skip_serializing, skip_deserializing)] ...), leaving the enum variants themselves intact; apply the same change to the other occurrence referenced (RawNode) so the Event::Notification and Event::RawNode variants serialize as unit-like variants rather than failing.tests/e2e/tests/memory_soak.rs (1)
409-409: 🧹 Nitpick | 🔵 TrivialConsolidate the repeated
w:gp2notification predicate.The same predicate appears in three places; extracting one helper avoids drift and keeps future node-API migrations safer.
♻️ Suggested refactor
+fn is_gp2_notification(e: &Event) -> bool { + matches!( + e, + Event::Notification(node) + if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2") + ) +} ... - .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) - }) + .wait_for_event(15, is_gp2_notification) .await?; ... - .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) - }) + .wait_for_event(15, is_gp2_notification) .await?; ... - .wait_for_event(15, |e| { - matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) - }) + .wait_for_event(15, is_gp2_notification) .await?;Also applies to: 429-429, 551-551
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/tests/memory_soak.rs` at line 409, Extract the repeated predicate matches!(e, Event::Notification(node) if node.get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) into a single helper function (e.g., fn is_w_gp2(node: &Node) -> bool or a closure) and replace the three inline occurrences with calls to that helper; locate uses around the Event::Notification matching logic in the tests (the match expression using node.get_attr("type") and "w:gp2") and ensure the helper is visible where the tests run so all three places (lines with the same predicate) call the new helper to avoid duplication.wacore/src/media_retry.rs (2)
178-182: 🧹 Nitpick | 🔵 TrivialKeep
msg_idborrowed in this ref-based parser.
into_owned()reintroduces an allocation on the zero-copy path even though the ID is only passed through to decryption and compared in-scope. A borrowedCow/&stris enough here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/media_retry.rs` around lines 178 - 182, The code currently calls into_owned() on the ID from node.get_attr("id"), causing an unnecessary allocation; keep msg_id as a borrowed &str (or a Cow::Borrowed) by removing into_owned() and propagating that borrowed type to the downstream decryption/comparison call sites (where msg_id is used) so those functions accept &str/Cow as needed; update the variable declaration for msg_id and any signatures that require an owned String to accept a borrowed &str/Cow instead.
208-213:⚠️ Potential issue | 🔴 CriticalValidate
enc_ivbefore decrypting.This path forwards untrusted
<enc_iv>bytes straight intodecrypt_media_retry_notification(), which still callsNonce::from_slice(iv). Inaes-gcm, a non-12-byte IV panics instead of returning an error, so a malformed notification can crash the parser. Add the length guard indecrypt_media_retry_notification()so every caller gets a handled error.🛡️ Suggested fix
pub fn decrypt_media_retry_notification( media_key: &[u8], stanza_id: &str, iv: &[u8], ciphertext: &[u8], ) -> Result<wa::MediaRetryNotification> { + if iv.len() != ENC_IV_SIZE { + return Err(anyhow!( + "invalid enc_iv length: expected {}, got {}", + ENC_IV_SIZE, + iv.len() + )); + } let key = derive_media_retry_key(media_key)?; let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?;In the RustCrypto aes-gcm crate, does Nonce::from_slice(iv) panic when iv.len() != 12 for Aes256Gcm?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/media_retry.rs` around lines 208 - 213, The code passes untrusted enc_iv bytes from encrypt_node into decrypt_media_retry_notification which calls Nonce::from_slice(iv) and can panic for non-12-byte IVs; modify decrypt_media_retry_notification to validate that iv.len() == 12 (or the required nonce length for Aes256Gcm) and return a clear Err(anyhow!(...)) when the length is wrong, rather than calling Nonce::from_slice on invalid input, so callers like the callsite using enc_iv (from encrypt_node.get_optional_child_by_tag) receive a handled error instead of crashing.src/client/device_registry.rs (1)
301-313:⚠️ Potential issue | 🟠 MajorPreserve the resolved
(user, server)pairs when deleting sessions.
lookup.all_keys()already loses which alias is LID vs PN, and this loop cross-products every key with bothServer::LidandServer::Pn. For a mapped user that can purge invalid combinations likepn@lid/lid@s.whatsapp.net, and in the worst case delete an unrelated session if the numeric identifiers collide. Match onUserLookupKeyshere and only delete the exact alias/server pairs that were actually resolved; forUnknown, use the caller's real server instead of both variants.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client/device_registry.rs` around lines 301 - 313, The current delete_sessions_for_devices implementation uses lookup.all_keys() and then pairs every key with both Server::Lid and Server::Pn, which can delete incorrect alias/server combinations; change delete_sessions_for_devices to iterate the resolved UserLookupKeys variants returned by resolve_lookup_keys(user) (inspect the enum/struct from resolve_lookup_keys) and for each variant construct the exact (user, server) pair when building Jid so you only call signal_cache.delete_session for the exact alias/server combinations originally resolved; for the Unknown variant, use the caller-provided server (do not try both Lid and Pn), and ensure you still loop over device_ids and call Jid::new and JidExt::to_protocol_address before signal_cache.delete_session.wacore/appstate/src/patch_decode.rs (1)
89-99:⚠️ Potential issue | 🟠 Major
NodeRefentry points still clone the whole tree.
parse_patch_list_ref()andparse_patch_lists_ref()immediately callnode.to_owned(), so every “zero-copy” parse still allocates the full subtree. That erases the main benefit of these APIs and makes the new_refsurface misleading. Please push the borrowed form throughparse_patch_list/parse_patch_lists/parse_single_collectioninstead of round-tripping throughNode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/appstate/src/patch_decode.rs` around lines 89 - 99, The new parse_patch_list_ref and parse_patch_lists_ref are defeating zero-copy by calling node.to_owned(); instead, make the borrowed NodeRef flow into the parsing pipeline: update or overload parse_patch_list / parse_patch_lists (or add internal helpers used by them and parse_single_collection) to accept &NodeRef<'_> and implement parsing logic on the borrowed NodeRef rather than converting to Node via to_owned(), then have parse_patch_list_ref and parse_patch_lists_ref call those NodeRef-taking functions; ensure symbols referenced are parse_patch_list_ref, parse_patch_lists_ref, parse_patch_list, parse_patch_lists, and parse_single_collection and remove the to_owned() round-trip so no full subtree allocation occurs.src/pair_code.rs (1)
238-245: 🧹 Nitpick | 🔵 TrivialAvoid heap-allocating the fixed 80-byte wrapped ephemeral.
This branch already rejects every length except 80, so
to_vec()adds unnecessary heap traffic on the pairing hot path. Parse it into[u8; 80]withtry_into()and pass that directly into the decrypt step.♻️ Minimal change
- let primary_wrapped_ephemeral = match reg_node + let primary_wrapped_ephemeral: [u8; 80] = match reg_node .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"]) .and_then(|n| match n.content.as_deref() { - Some(NodeContentRef::Bytes(b)) if b.len() == 80 => Some(b.to_vec()), + Some(NodeContentRef::Bytes(b)) => b.as_ref().try_into().ok(), _ => None, }) { Some(b) => b,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair_code.rs` around lines 238 - 245, The code currently heap-allocates the 80-byte wrapped ephemeral by calling to_vec() in the match for reg_node.get_optional_child_by_tag(...)->NodeContentRef::Bytes, which is wasteful because lengths != 80 are already rejected; change the match to convert the slice to a fixed-size array using try_into() (i.e., parse into [u8; 80]) and bind that array to primary_wrapped_ephemeral so it can be passed by value into the subsequent decrypt step (remove the unnecessary Vec allocation and adjust downstream uses to accept [u8; 80]).wacore/derive/src/lib.rs (1)
185-194:⚠️ Potential issue | 🟠 MajorGenerated JID parsing still drops parse errors.
optional_jid()can record a malformed attribute and returnNone; because the generated code never finalizes that parser state, optional JID fields silently discard invalid wire data and required JID fields misreport it as “missing”. Please generate a single attr-parser binding here and propagate itsfinish()error path before constructingSelf.Also applies to: 329-335
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/derive/src/lib.rs` around lines 185 - 194, The generated code for AttrType::Jid must not call optional_jid() or jid parsing inline; instead bind the attribute parser once (e.g., let parsed = node.attrs().optional_jid(`#attr_name`) or node.attrs().jid(`#attr_name`)), then call parsed.finish().map_err(|e| ::anyhow::anyhow!(...))? to propagate parsing errors before using the value to construct the field (`#field_ident`); do this for both the required and optional branches so malformed JIDs produce a proper finish() error instead of returning None or being treated as "missing".wacore/src/iq/usync.rs (1)
121-128: 🧹 Nitpick | 🔵 TrivialUse
optional_jid()in the remaining JID attribute paths.These branches still stringify and re-parse JIDs, so
ValueRef::Jidresponses lose the zero-copy win and pay an extra allocation/parse cycle. Please switch theval,jid, andpn_jidreads to direct JID extraction as well.Also applies to: 141-145, 316-326, 558-561
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/usync.rs` around lines 121 - 128, The parse_lid_jid function (and the other JID attribute branches reading "val", "jid", and "pn_jid") currently call .attrs().optional_string(...).and_then(|s| s.parse::<Jid>().ok()), causing extra allocation/parse; replace those chains with the zero-copy accessor .attrs().optional_jid("<attr_name>") so you return Option<Jid> directly (e.g., in parse_lid_jid use get_optional_child("lid").and_then(|n| n.attrs().optional_jid("val"))), and apply the same change to the other helper/sites that read "jid" and "pn_jid" so all JID reads use optional_jid() and keep the return types as Option<Jid>.wacore/src/iq/blocklist.rs (1)
116-131: 🧹 Nitpick | 🔵 TrivialReuse
BlocklistResponse::try_from_node_refhere.Lines 117-131 duplicate the same
<list>/direct-<item>traversal and warning behavior already implemented inBlocklistResponse. Keeping both copies makes future schema tweaks easy to fix in one path and miss in the other.Minimal cleanup
fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> { - // BlocklistResponse checks for a <list> child or direct <item> children - let entries = if let Some(list) = response.get_optional_child("list") { - list.get_children_by_tag("item") - } else { - response.get_children_by_tag("item") - } - .filter_map(|item| match BlocklistEntry::try_from_node_ref(item) { - Ok(entry) => Some(entry), - Err(e) => { - warn!(target: "blocklist", "Failed to parse blocklist entry: {e}"); - None - } - }) - .collect(); - Ok(entries) + Ok(BlocklistResponse::try_from_node_ref(response)?.entries) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/blocklist.rs` around lines 116 - 131, The parse_response implementation duplicates the <list>/<item> traversal and error-handling already present in BlocklistResponse::try_from_node_ref; replace the manual traversal in parse_response with a call to BlocklistResponse::try_from_node_ref (or the existing BlocklistResponse helper) to obtain the entries, preserving its warning behavior and result type so future schema changes are centralized; update parse_response to return the result from that helper and remove the duplicated filtering/collect logic currently using BlocklistEntry::try_from_node_ref and warn!.wacore/src/iq/chatstate.rs (1)
140-147:⚠️ Potential issue | 🟡 MinorMalformed
fromJIDs still collapse intoMissingFrom.Lines 141-147 use
optional_jid("from"), so a badfromvalue is indistinguishable from a missing one unless the parser explicitly inspects recorded attr errors. That leavesChatstateParseError::InvalidJideffectively unreachable.Run this to verify the attr parser behavior. Expected result: if
optional_jid()records parse failures and returnsNone,parse()should switch to a required JID parse or inspect the recorded errors before returningMissingFrom/SelfEcho.#!/bin/bash set -euo pipefail echo "=== optional_jid implementation ===" fd -a 'attrs.rs' | while read -r f; do rg -n 'fn optional_jid\(' "$f" -A14 -B4 done echo "=== chatstate parse() context ===" sed -n '135,155p' wacore/src/iq/chatstate.rs echo "=== attr error inspection in chatstate parser ===" rg -n '\.errors\(|attrs\.errors' wacore/src/iq/chatstate.rs -A2 -B2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/chatstate.rs` around lines 140 - 147, The parser uses attrs.optional_jid("from") so malformed JIDs are treated the same as missing ones and never yield ChatstateParseError::InvalidJid; update parse() in chatstate.rs to detect parse failures from attrs (either call attrs.required_jid("from") when optional_jid returned None but attrs.errors() contains a JID parse error, or inspect attrs.errors() after optional_jid to convert recorded JID parse errors into ChatstateParseError::InvalidJid), ensuring you still preserve the SelfEcho branch when "to" is present; reference the functions/methods optional_jid, required_jid (if available), attrs.errors(), and the error variants ChatstateParseError::InvalidJid, ::MissingFrom, ::SelfEcho when making the change.src/retry.rs (1)
532-567: 🧹 Nitpick | 🔵 TrivialDon’t resolve the requester JID twice.
The caller already resolves this on Line 223, but Line 566 resolves it again inside
process_retry_key_bundle(). That adds another lookup on every retry and makes the parameter name misleading.Minimal cleanup
async fn process_retry_key_bundle( &self, node: &NodeRef<'_>, - requester_jid: &wacore_binary::Jid, + resolved_jid: &wacore_binary::Jid, is_peer: bool, ) -> Result<(), anyhow::Error> { @@ - let resolved_jid = self.resolve_encryption_jid(requester_jid).await; let signal_address = resolved_jid.to_protocol_address(); @@ - u32::from(requester_jid.device).into(), + u32::from(resolved_jid.device).into(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/retry.rs` around lines 532 - 567, process_retry_key_bundle currently re-resolves the requester JID with self.resolve_encryption_jid(requester_jid).await even though the caller already resolved it; change the function to accept the already-resolved value (e.g., a resolved_jid or signal_address) instead of resolving internally, remove the duplicate call and the local resolved_jid variable, and update all call sites that invoke process_retry_key_bundle to pass the pre-resolved value (or its to_protocol_address()) so the misleading parameter name requester_jid is consistent with its usage and no extra lookup occurs; keep the function name process_retry_key_bundle and update parameter list and references inside the function accordingly.wacore/src/stanza/devices.rs (1)
317-321:⚠️ Potential issue | 🟠 MajorReject device notifications without an
id.Line 320 turns a missing or malformed
idinto"". That loses the stanza handle this struct is supposed to carry and makes downstream ACK/dedup logic ambiguous. Parse it as required instead of defaulting.Minimal fix
- let stanza_id = node - .get_attr("id") - .map(|v| v.as_str()) - .unwrap_or_default() - .into_owned(); + let stanza_id = required_attr(node, "id")?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/stanza/devices.rs` around lines 317 - 321, The code currently defaults a missing or invalid stanza id to an empty string (the stanza_id assignment using node.get_attr("id").unwrap_or_default()), which loses the stanza handle; change this to treat id as required: when node.get_attr("id") is None or not valid, return an error or early-none (i.e., propagate Result::Err or Option::None from the surrounding function) instead of calling into_owned on an empty default so the calling ACK/dedup logic can detect and reject device notifications without an id; update the surrounding function signature/return path that constructs the Stanza/device struct accordingly and reference the stanza_id variable and node.get_attr("id") call when making the change.src/handlers/notification.rs (1)
555-558:⚠️ Potential issue | 🟠 MajorUse a fallible conversion for
key-index.Line 557 narrows an untrusted
u64withas u32, so oversized wire values silently wrap and can store the wrong key index for a companion device. Drop only the invalidkey-indexinstead of corrupting it.Minimal fix
.filter_map(|n| { let jid = n.attrs().optional_jid("jid")?; - let key_index = n.attrs().optional_u64("key-index").map(|v| v as u32); + let key_index = n + .attrs() + .optional_u64("key-index") + .and_then(|v| u32::try_from(v).ok()); Some(AccountSyncDevice { jid, key_index }) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/handlers/notification.rs` around lines 555 - 558, The closure that builds AccountSyncDevice currently narrows an untrusted u64 to u32 with `as`, which silently wraps oversized values; change the conversion to a fallible one so invalid key-index values are dropped. Replace the `.map(|v| v as u32)` conversion on `n.attrs().optional_u64("key-index")` with a fallible conversion (e.g., use `and_then(|v| v.try_into().ok())` or check `v <= u32::MAX`) so `key_index` becomes `None` for out-of-range values, leaving the rest of the `filter_map` and `AccountSyncDevice { jid, key_index }` logic intact.src/pair.rs (1)
195-200:⚠️ Potential issue | 🟠 MajorReject missing or malformed pair-success
jid/lidinstead of defaulting them.
optional_jid(...).unwrap_or_default()still lets an invalid<device>block flow intoSetId/SetLid, so pairing can persist empty account identifiers and look successful.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pair.rs` around lines 195 - 200, The code currently defaults missing/malformed device jid/lid via parser.optional_jid(...).unwrap_or_default(), allowing empty/invalid IDs to proceed; instead, validate and reject the pair-success when jid or lid are missing or malformed: in the block handling success_node/get_optional_child_by_tag("device") inspect parser.optional_jid("jid") and parser.optional_jid("lid") and if either returns Err or None propagate an error/return early (do not use unwrap_or_default), so SetId/SetLid are only called with valid parsed_jid and parsed_lid.src/message.rs (1)
376-383: 🛠️ Refactor suggestion | 🟠 MajorCustom enc handlers still force a full
<enc>clone.
to_owned()rematerializes the payload right before dispatch, so the zero-copy migration stops at the custom-handler boundary. Since this PR is already carrying breaking API changes, this is a good point to switch the handler trait to borrowedNodeRef/OwnedNodeRefand only clone in handlers that truly need ownership.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/message.rs` around lines 376 - 383, The code currently calls enc_node.to_owned() and enc_type.to_string() before invoking handler_clone.handle, forcing a full <enc> clone; change the handler trait to accept borrowed references (e.g., NodeRef and an optional OwnedNodeRef) so you can pass a borrowed &NodeRef into handler_clone.handle without rematerializing the payload, and only call .to_owned() (or construct OwnedNodeRef) inside the handler implementations that need ownership; update the trait signature (handle) and all implementors to accept &NodeRef (and provide an OwnedNodeRef type for callers that truly require ownership), then remove enc_node.to_owned() and pass the borrowed enc_node directly in the runtime.spawn closure.wacore/src/iq/node.rs (1)
22-25: 🧹 Nitpick | 🔵 TrivialKeep
required_attrzero-copy.Returning
Stringhere forces an allocation on every required attribute read and pushes callers off the borrowed path.Cow<'_, str>would matchoptional_attrand the rest of this migration better.♻️ Minimal diff
-pub(crate) fn required_attr(node: &NodeRef<'_>, key: &str) -> Result<String, anyhow::Error> { - node.get_attr(key) - .map(|v| v.to_string()) +pub(crate) fn required_attr<'a>( + node: &'a NodeRef<'_>, + key: &str, +) -> Result<Cow<'a, str>, anyhow::Error> { + node.attrs() + .optional_string(key) .ok_or_else(|| anyhow!("missing required attribute {key}")) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/node.rs` around lines 22 - 25, required_attr currently forces an owned String; change it to return Result<std::borrow::Cow<'_, str>, anyhow::Error> to keep zero-copy like optional_attr: update the function signature of required_attr to Result<Cow<'_, str>, anyhow::Error>, map the Option<&str> from node.get_attr(key) into a Cow (borrowed) instead of to_string(), and keep the same ok_or_else missing-attribute error; also update callers (if any) to accept a Cow<'_, str> rather than String.wacore/src/iq/groups.rs (1)
379-382:⚠️ Potential issue | 🟠 MajorStop coercing unknown participant types to
Member.This brings back the silent downgrade path: an unexpected
typenow parses asMemberinstead of failing, which can erase admin/superadmin state in group metadata.🐛 Proposed fix
- let participant_type = attrs - .optional_string("type") - .and_then(|s| ParticipantType::try_from(s.as_ref()).ok()) - .unwrap_or(ParticipantType::Member); + let participant_type = + ParticipantType::try_from(attrs.optional_string("type").as_deref())?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/groups.rs` around lines 379 - 382, The code silently coerces unknown participant types to Member by ending the chain with .unwrap_or(ParticipantType::Member); change this so invalid values do not get downgraded: remove the .unwrap_or(...) and instead propagate a parse error when ParticipantType::try_from(...) fails (i.e., treat invalid strings as an error rather than defaulting), by mapping the Result from ParticipantType::try_from into the function's error path (or returning a descriptive Err) where participant_type is constructed from attrs.optional_string and ParticipantType::try_from.src/client.rs (2)
4900-4902: 🧹 Nitpick | 🔵 TrivialDrop the redundant
node_to_owned_refshim.This wrapper only forwards to
crate::test_utils::node_to_owned_refunchanged, so it is just another helper to keep in sync.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 4900 - 4902, The function node_to_owned_ref currently just forwards to crate::test_utils::node_to_owned_ref and is redundant; delete the shim fn node_to_owned_ref(...) from this module and update any callers in this file to call crate::test_utils::node_to_owned_ref(&node) (or pass ownership/signature as needed) so there’s a single canonical implementation to maintain; ensure imports/usages compile after removing the wrapper.
2299-2317:⚠️ Potential issue | 🟡 MinorDon’t remove the ACK waiter before the fallible re-materialization.
response_waiters.remove(&id)happens beforemarshal_ref(...)/OwnedNodeRef::new(...). If that conversion fails, the caller only gets a canceled channel and loses the real failure cause. Materialize first, or change the waiter payload to carry an explicit error.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/client.rs` around lines 2299 - 2317, The code removes the ACK waiter via self.response_waiters.lock().await.remove(&id) before performing the fallible re-materialization (wacore_binary::marshal::marshal_ref and wacore_binary::OwnedNodeRef::new), losing the real error if conversion fails; change the flow to perform marshal_ref(...) and OwnedNodeRef::new(...) first and only after successful Ok(onr) lock and remove the waiter and send Arc::new(onr), or alternatively change the waiter payload type used by response_waiters to carry a Result<OwnedNodeRef, Error> so you can remove the waiter early but send Err(...) on failure; update usages of waiter.send(...) accordingly to send the proper success or error value.wacore/src/iq/mediaconn.rs (1)
303-317:⚠️ Potential issue | 🟠 MajorFinish attribute parsing before returning media-conn responses.
optional_u64()records bad numeric attrs on the parser. Both functions readttl/auth_ttl/max_buckets/set_ip_tokenand return withoutattrs.finish(), so malformed values are silently coerced to0/None.🐛 Suggested fix
let ttl = attrs.optional_u64("ttl").unwrap_or(0); let auth_ttl = attrs.optional_u64("auth_ttl"); let max_buckets = attrs.optional_u64("max_buckets"); let ip_token = attrs.optional_string("ip_token").map(|s| s.into_owned()); let set_ip_token = attrs.optional_u64("set_ip_token"); + attrs.finish()?;let ttl = attrs.optional_u64("ttl").unwrap_or(0); let auth_ttl = attrs.optional_u64("auth_ttl"); let max_buckets = attrs.optional_u64("max_buckets"); + attrs.finish()?;Also applies to: 364-371
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/iq/mediaconn.rs` around lines 303 - 317, In try_from_node_ref (and the similar parsing block around lines 364-371) finish parsing attributes by calling attrs.finish() before returning the constructed MediaConn; if attrs.finish() returns an error propagate or convert it into an anyhow::Error and return it so malformed numeric attributes (from optional_u64/optional_string) are surfaced instead of being silently coerced to 0/None. Ensure the attrs.finish() call is placed after reading ttl/auth_ttl/max_buckets/set_ip_token/ip_token and before Ok(...) so parser-recorded errors are handled.wacore/binary/src/decoder.rs (1)
109-115:⚠️ Potential issue | 🟡 MinorAdd a regression test for unknown
JID_PAIRservers.The fail-fast behavior lives here now, but the decoder tests still never assert that an invalid server returns
BinaryError::AttrParse(..). Without that coverage, the oldServer::Pnfallback can slip back in unnoticed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/decoder.rs` around lines 109 - 115, Add a regression test that verifies read_jid_pair returns BinaryError::AttrParse when the server string is invalid: construct input bytes that exercise read_value_as_string() twice (first for user, second for a non‑standard server token), call decoder.read_jid_pair(), and assert the result is Err(BinaryError::AttrParse(_)); this ensures the Server::try_from(...) path in read_jid_pair triggers the error rather than falling back to Server::Pn.wacore/binary/src/jid.rs (1)
63-79:⚠️ Potential issue | 🟠 MajorReject malformed numeric parts on the fast path instead of coercing them to zero.
parse_jid_fast()still accepts bad device/agent components by falling back to0, so inputs like123:abc@s.whatsapp.netoruser.300@hostedcan bypass the validating fallback and turn into different valid JIDs. These branches should returnNoneon parse failure/out-of-range soFromStrfalls back to the strict path.Also applies to: 84-89, 121-146
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/binary/src/jid.rs` around lines 63 - 79, The fast-path in parse_jid_fast currently coerces malformed numeric parts to 0 (e.g., device/agent/integrator) which lets invalid inputs slip through; change the parsing branches in parse_jid_fast (the HIDDEN_USER_SERVER branch that builds ParsedJidParts and the similar numeric parses at the other fast-path locations referenced) to attempt numeric parsing with proper range checks and return None on parse errors or out-of-range values instead of using unwrap_or(0), so FromStr will fall back to the strict parsing path when any numeric component is invalid.wacore/src/prekeys.rs (2)
137-139: 🧹 Nitpick | 🔵 TrivialDrop the
_refsuffix from the remaining private prekey helpers.At this point
&NodeRef<'_>is the only code path here, so the suffix just adds noise and makes this migration feel half-finished.Also applies to: 211-263
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wacore/src/prekeys.rs` around lines 137 - 139, Rename the remaining helper functions that end with the _ref suffix (e.g., node_to_pre_key_bundle_ref) to drop the suffix (e.g., node_to_pre_key_bundle), keep their current signatures (they still take &NodeRef<'_>), and update all internal call sites to use the new names so the migration is consistent; apply the same rename pattern to the other private prekey helper functions referenced in the same region (the helpers in the 211–263 block) ensuring no public API changes and running tests to confirm nothing else references the old names.
144-147:⚠️ Potential issue | 🟠 MajorKeep the prekey parser zero-copy through the fixed-size byte fields.
These branches still
to_vec()borrowedNodeContentRef::Bytesand then immediately length-check/copy them into arrays. That gives back heap traffic right in the middle of the zero-copy migration and on a hot path the PR is explicitly optimizing.Also applies to: 241-257, 270-286
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 93df1c6b-f4d8-4f76-8fd4-63c717290232
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (120)
.gitignoreCargo.tomlsrc/client.rssrc/client/context_impl.rssrc/client/device_registry.rssrc/client/lid_pn.rssrc/client/sender_keys.rssrc/client/sessions.rssrc/features/blocking.rssrc/features/chat_actions.rssrc/features/chatstate.rssrc/features/community.rssrc/features/contacts.rssrc/features/groups.rssrc/features/media_reupload.rssrc/features/newsletter.rssrc/features/polls.rssrc/features/presence.rssrc/features/signal.rssrc/features/status.rssrc/features/tctoken.rssrc/handlers/basic.rssrc/handlers/chatstate.rssrc/handlers/ib.rssrc/handlers/iq.rssrc/handlers/message.rssrc/handlers/notification.rssrc/handlers/presence.rssrc/handlers/receipt.rssrc/handlers/router.rssrc/handlers/traits.rssrc/handlers/unimplemented.rssrc/history_sync.rssrc/jid_utils.rssrc/keepalive.rssrc/lib.rssrc/message.rssrc/pair.rssrc/pair_code.rssrc/pdo.rssrc/pending_device_sync.rssrc/prekeys.rssrc/receipt.rssrc/request.rssrc/retry.rssrc/send.rssrc/sender_key_device_cache.rssrc/session.rssrc/spam_report.rssrc/test_utils.rssrc/types/enc_handler.rssrc/unified_session.rssrc/usync.rstests/e2e/src/lib.rstests/e2e/tests/memory_soak.rstests/e2e/tests/offline_groups.rstests/e2e/tests/presence.rstests/e2e/tests/privacy_tokens.rswacore/appstate/src/patch_decode.rswacore/benches/send_receive_benchmark.rswacore/binary/Cargo.tomlwacore/binary/benches/binary_benchmark.rswacore/binary/src/attrs.rswacore/binary/src/decoder.rswacore/binary/src/jid.rswacore/binary/src/lib.rswacore/binary/src/node.rswacore/derive/src/lib.rswacore/src/appstate_sync.rswacore/src/client/context.rswacore/src/ib.rswacore/src/iq/blocklist.rswacore/src/iq/business.rswacore/src/iq/chatstate.rswacore/src/iq/contacts.rswacore/src/iq/devices.rswacore/src/iq/dirty.rswacore/src/iq/groups.rswacore/src/iq/keepalive.rswacore/src/iq/mediaconn.rswacore/src/iq/mex.rswacore/src/iq/node.rswacore/src/iq/passive.rswacore/src/iq/prekeys.rswacore/src/iq/privacy.rswacore/src/iq/profile.rswacore/src/iq/props.rswacore/src/iq/spam_report.rswacore/src/iq/spec.rswacore/src/iq/tctoken.rswacore/src/iq/usync.rswacore/src/media_retry.rswacore/src/message_processing.rswacore/src/messages.rswacore/src/pair.rswacore/src/pair_code.rswacore/src/prekeys.rswacore/src/proto_helpers.rswacore/src/protocol/mod.rswacore/src/protocol/retry.rswacore/src/reporting_token.rswacore/src/request.rswacore/src/send.rswacore/src/session.rswacore/src/stanza/business.rswacore/src/stanza/devices.rswacore/src/stanza/groups.rswacore/src/stanza/message.rswacore/src/stanza/notification.rswacore/src/stanza/receipt.rswacore/src/store/commands.rswacore/src/store/device.rswacore/src/types/call.rswacore/src/types/events.rswacore/src/types/jid.rswacore/src/types/message.rswacore/src/types/spam_report.rswacore/src/usync.rswacore/src/xml.rswacore/tests/binary_protocol_test.rs
| client | ||
| .runtime | ||
| .spawn(Box::pin(async move { | ||
| client_clone | ||
| .persistence_manager | ||
| .modify_device(|device| { | ||
| device.edge_routing_info = Some(routing_bytes); | ||
| }) | ||
| .await; | ||
| })) |
There was a problem hiding this comment.
Persist edge-routing info through the device command path, not modify_device().
This directly mutates Device state from the handler path. That bypasses the repo’s required serialization/update mechanism and makes this write inconsistent with the rest of the state-management flow. As per coding guidelines "Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() and read via get_device_snapshot()".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/handlers/ib.rs` around lines 122 - 131, The code currently mutates
Device.edge_routing_info inside the async spawn by calling
persistence_manager.modify_device(), which bypasses the repo's
serialization/update flow; replace that direct mutation with constructing the
appropriate DeviceCommand that sets edge_routing_info and submit it to
persistence_manager.process_command() (await its completion) inside the spawned
task (use client_clone and persistence_manager as before), and ensure any reads
rely on persistence_manager.get_device_snapshot() rather than reading Device
directly; remove the modify_device() usage entirely and use DeviceCommand +
PersistenceManager::process_command() to persist the change.
| fn is_empty(&self) -> bool { | ||
| self.server().is_empty() | ||
| self.user().is_empty() |
There was a problem hiding this comment.
is_empty() now misclassifies valid server-only JIDs as empty.
This file explicitly treats server-only JIDs as valid, but self.user().is_empty() returns true for all of them. That already has a downstream effect in wacore/src/request.rs Line 184, where !target.is_empty() gates whether the IQ target attribute is emitted—so a legitimate server-only target will now be dropped.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/binary/src/jid.rs` around lines 356 - 357, is_empty() currently
returns self.user().is_empty(), which treats server-only JIDs as empty; change
it to consider both parts so a JID is empty only if both user() and server() are
empty—i.e., update is_empty() to return self.user().is_empty() &&
self.server().is_empty() (use the existing user() and server() accessors so
downstream code like the IQ target emission in request.rs correctly recognizes
server-only JIDs).
| let member_add_mode = node | ||
| .get_optional_child_by_tag(&["member_add_mode"]) | ||
| .and_then(|n| match &n.content { | ||
| Some(NodeContent::String(s)) => MemberAddMode::try_from(s.as_str()).ok(), | ||
| .and_then(|n| match n.content.as_deref() { | ||
| Some(NodeContentRef::String(s)) => MemberAddMode::try_from(s.as_ref()).ok(), | ||
| _ => None, | ||
| }); | ||
|
|
||
| let member_link_mode = node | ||
| .get_optional_child_by_tag(&["member_link_mode"]) | ||
| .and_then(|n| match &n.content { | ||
| Some(NodeContent::String(s)) => MemberLinkMode::try_from(s.as_str()).ok(), | ||
| .and_then(|n| match n.content.as_deref() { | ||
| Some(NodeContentRef::String(s)) => MemberLinkMode::try_from(s.as_ref()).ok(), | ||
| _ => None, | ||
| }); |
There was a problem hiding this comment.
Don't treat invalid member mode values as if the field were absent.
Both parsers swallow enum conversion failures with .ok(). If the server sends an unexpected member_add_mode or member_link_mode, callers now see None and lose the actual group permission state instead of getting a parse error.
🐛 Proposed fix
- let member_add_mode = node
- .get_optional_child_by_tag(&["member_add_mode"])
- .and_then(|n| match n.content.as_deref() {
- Some(NodeContentRef::String(s)) => MemberAddMode::try_from(s.as_ref()).ok(),
- _ => None,
- });
+ let member_add_mode = match node.get_optional_child_by_tag(&["member_add_mode"]) {
+ Some(n) => match n.content.as_deref() {
+ Some(NodeContentRef::String(s)) => Some(MemberAddMode::try_from(s.as_ref())?),
+ Some(_) => return Err(anyhow!("expected string content in <member_add_mode>")),
+ None => None,
+ },
+ None => None,
+ };
- let member_link_mode = node
- .get_optional_child_by_tag(&["member_link_mode"])
- .and_then(|n| match n.content.as_deref() {
- Some(NodeContentRef::String(s)) => MemberLinkMode::try_from(s.as_ref()).ok(),
- _ => None,
- });
+ let member_link_mode = match node.get_optional_child_by_tag(&["member_link_mode"]) {
+ Some(n) => match n.content.as_deref() {
+ Some(NodeContentRef::String(s)) => Some(MemberLinkMode::try_from(s.as_ref())?),
+ Some(_) => return Err(anyhow!("expected string content in <member_link_mode>")),
+ None => None,
+ },
+ None => None,
+ };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@wacore/src/iq/groups.rs` around lines 617 - 629, The parsers for
member_add_mode and member_link_mode currently swallow enum conversion failures
by calling .ok() on MemberAddMode::try_from and MemberLinkMode::try_from,
treating invalid values as absent; change the logic in the parsing code that
uses get_optional_child_by_tag (the member_add_mode and member_link_mode parsing
blocks) to propagate a parse error instead of returning None — e.g. inspect
NodeContentRef::String(s), call try_from(s.as_ref()) and map the Err into the
function's error/Result (or use the ? operator) so invalid server-sent values
produce an error rather than quietly becoming None.
Replace Arc<Node> with Arc<OwnedNodeRef> throughout the handler chain. OwnedNodeRef wraps Yoke<NodeRef<'static>, Vec<u8>>, keeping the decoded NodeRef borrowing directly from the decompressed buffer — zero allocation for attribute keys, values, and byte content. Changes: - Add yoke dependency, derive Yokeable on NodeRef/ValueRef/JidRef/NodeContentRef - Define OwnedNodeRef: self-referential zero-copy node via Yoke - decrypt_frame() returns OwnedNodeRef instead of Node - StanzaHandler trait takes Arc<OwnedNodeRef> - All handlers use node.get() to access &NodeRef - Client methods accept &NodeRef<'_> instead of &Node - Event::Notification and Event::RawNode use Arc<OwnedNodeRef> - NodeWaiter/NodeFilter adapted to OwnedNodeRef - Message handler channel uses Arc<OwnedNodeRef> - Message processing uses &NodeRef throughout - IQ response waiters still use Node via .to_owned_node() (infrequent) - Re-export OwnedNodeRef and Server from whatsapp-rust for clean API BREAKING CHANGE: Handler trait signature changes from Arc<Node> to Arc<OwnedNodeRef>. Event::Notification wraps Arc<OwnedNodeRef> instead of Node. Event::RawNode wraps Arc<OwnedNodeRef> instead of Arc<Node>. Access node data via node.get() which returns &NodeRef. Use .to_owned_node() when an owned Node is needed.
c035344 to
916b6ff
Compare
Summary
Three major performance optimizations that eliminate allocation overhead in the node decode path:
Jid Server enum: Replace
Jid { server: Cow<'static, str> }with aServeru8 enum. Shrinks Jid from 56→32 bytes. Server comparisons becomeu8==u8instead of string comparison.Yoke zero-copy decoding: Replace
Arc<Node>withArc<OwnedNodeRef>throughout the entire handler chain.OwnedNodeRefwrapsYoke<NodeRef<'static>, Vec<u8>>, keeping the decodedNodeRefborrowing directly from the decompressed buffer — zero allocation for attribute keys, values, and byte content.Full NodeRef migration + API ergonomics: Flipped the
ProtocolNodetrait sotry_from_node_ref(&NodeRef)is the required method. Unified API naming —NodeRef::attrs()matchesNode::attrs(),ValueRef::as_str()returnsCow<str>likeNodeValue::as_str(). Added delegation methods toOwnedNodeRefso handlers can callnode.attrs()directly without.get().Breaking Changes
1. Jid.server:
Cow<'static, str>→ServerenumType change:
Jid { server: Cow<'static, str> }→Jid { server: Server }Same for:
JidRef { server: Cow<'a, str> }→JidRef { server: Server }cow_server_from_str()deleted — useServer::try_from(s)insteadJidError)Serverserializes as string ("s.whatsapp.net") for backward-compatible JSONPn,Lid,Group,Broadcast,Newsletter,Hosted,HostedLid,Messenger,Interop,Bot,Legacy2. Handler trait:
Arc<Node>→Arc<OwnedNodeRef>OwnedNodeRefdelegates:tag(),attrs(),get_attr(),children(),get_optional_child(),get_optional_child_by_tag(),get_children_by_tag(),content_bytes(),content_str(),content_nodes().Use
.get()only when you need the full&NodeRef(e.g., passing to functions).3. Unified API — NodeRef matches Node
node.attrs()node.attrs()node.attrs()node.attrs.get("k")node.get_attr("k")node.get_attr("k")node.children()node.children()node.children()node.get_optional_child("t")node.content → NodeContent::Bytesnode.content_bytes()node.content_bytes()node.content_as_string()node.content_str()node.content_str()4. ValueRef::as_str() now matches NodeValue::as_str()
to_string_cow()deleted —as_str()does the same thing now.5. Event types
6. ProtocolNode trait flipped
7. IqSpec::parse_response
8. Helper functions renamed (dropped
_refsuffix)All
iq::nodehelpers take&NodeRefas canonical signature:required_child,optional_child,required_attr,optional_attr,collect_childrenStanza parsers:
DeviceNotification::try_parse,BusinessNotification::try_parse,parse_lid_mappings_from_response,parse_prekeys_response9. New public re-exports
whatsapp_rust::{Server, OwnedNodeRef, CompactString, Jid}wacore_binary::{Server, Jid, JidRef, JidExt, Node, NodeRef, NodeValue, OwnedNodeRef, NodeContent, NodeContentRef, Attrs, CompactString, AttrParser, AttrParserRef, DeviceKey, ...}What's zero-copy and what's not
Fully zero-copy (no allocation on decode path):
decrypt_frame→OwnedNodeRef(yoke wraps decompressed buffer)Arc<OwnedNodeRef>(cheap refcount)parse_responseimplementationsparse_message_info, encryption, routing)Structurally required
to_owned()(7 calls):GroupNotificationAction::Create/Link/Unlink { raw: Node }— struct fieldStreamError.raw/ConnectFailure.raw— event fieldsIqError::Disconnected(Node)—'staticerror typeenc_nodeclone crossing async task boundaryTest plan
cargo fmt --allcargo clippy --all --tests— zero errors, zero warningscargo test -p wacore-binary -p wacore -p whatsapp-rust— all passingcargo check -p wacore-binary --all-features— serde verifiedSummary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests